├── .gitignore ├── CHANGELOG ├── CONTRIBUTING.md ├── Cargo.toml ├── INSTALL.md ├── LICENSE ├── README.md ├── SECURITY.md ├── airup-sdk ├── Cargo.toml ├── build.rs └── src │ ├── blocking │ ├── files │ │ ├── milestone.rs │ │ ├── mod.rs │ │ └── system_conf.rs │ ├── fs.rs │ ├── mod.rs │ └── rpc.rs │ ├── build.rs │ ├── debug.rs │ ├── error.rs │ ├── extapi.rs │ ├── extension │ └── mod.rs │ ├── ffi │ └── mod.rs │ ├── files │ ├── milestone.rs │ ├── mod.rs │ ├── service.rs │ └── system_conf.rs │ ├── info.rs │ ├── lib.rs │ ├── nonblocking │ ├── files │ │ ├── milestone.rs │ │ ├── mod.rs │ │ └── system_conf.rs │ ├── fs.rs │ ├── mod.rs │ └── rpc.rs │ ├── prelude.rs │ ├── rpc.rs │ ├── system.rs │ └── util.rs ├── airup ├── Cargo.toml └── src │ ├── daemon.rs │ ├── debug.rs │ ├── disable.rs │ ├── edit.rs │ ├── enable.rs │ ├── main.rs │ ├── query.rs │ ├── reboot.rs │ ├── reload.rs │ ├── restart.rs │ ├── self_reload.rs │ ├── start.rs │ ├── stop.rs │ ├── trigger_event.rs │ └── util.rs ├── airupd ├── Cargo.toml └── src │ ├── ace │ ├── builtins.rs │ ├── mod.rs │ └── parser.rs │ ├── app.rs │ ├── env.rs │ ├── events.rs │ ├── extension.rs │ ├── lifetime.rs │ ├── logging.rs │ ├── main.rs │ ├── milestones │ ├── early_boot.rs │ ├── mod.rs │ └── reboot.rs │ ├── rpc │ ├── api │ │ ├── debug.rs │ │ ├── info.rs │ │ ├── mod.rs │ │ ├── session.rs │ │ └── system.rs │ └── mod.rs │ ├── storage │ ├── config.rs │ ├── milestones.rs │ ├── mod.rs │ ├── runtime.rs │ └── services.rs │ └── supervisor │ ├── mod.rs │ └── task │ ├── cleanup.rs │ ├── health_check.rs │ ├── mod.rs │ ├── reload.rs │ ├── start.rs │ └── stop.rs ├── airupfx ├── airupfx-env │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── users.rs ├── airupfx-extensions │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── airupfx-fs │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── unix.rs ├── airupfx-io │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── line_piper.rs ├── airupfx-isolator │ ├── Cargo.toml │ └── src │ │ ├── fallback.rs │ │ ├── lib.rs │ │ └── linux.rs ├── airupfx-macros │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── airupfx-power │ ├── Cargo.toml │ └── src │ │ ├── common.rs │ │ ├── freebsd.rs │ │ ├── lib.rs │ │ ├── linux.rs │ │ ├── macos.rs │ │ └── unix.rs ├── airupfx-process │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── unix.rs ├── airupfx-signal │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── unix.rs ├── airupfx-time │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── airupfx │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── prelude.rs │ └── util.rs ├── docs ├── README.md ├── artwork │ ├── LICENSE │ ├── airup_logo.png │ ├── airup_logo_320x200.png │ └── airup_logo_640x400.png ├── en-US │ ├── admin_manual │ │ ├── airs_format.md │ │ ├── index.md │ │ ├── linux_distro_tutorial.md │ │ └── standalone_supervisor.md │ ├── api_manual │ │ ├── c │ │ │ ├── airup_h.md │ │ │ └── index.md │ │ ├── predefined_events │ │ │ └── index.md │ │ └── rpc │ │ │ ├── debug.md │ │ │ ├── index.md │ │ │ ├── info.md │ │ │ └── system.md │ ├── index.md │ └── man_pages │ │ ├── airup.md │ │ └── index.md ├── examples │ └── index.md ├── i18n_guide.md ├── resources │ ├── airup-fallback-logger.airs │ ├── airupd.airs │ └── build_manifest.json └── zh-CN │ ├── admin_manual │ ├── airs_format.md │ ├── index.md │ ├── linux_distro_tutorial.md │ └── standalone_supervisor.md │ ├── api_manual │ ├── c │ │ ├── airup_h.md │ │ └── index.md │ ├── predefined_events │ │ └── index.md │ └── rpc │ │ ├── debug.md │ │ ├── index.md │ │ ├── info.md │ │ └── system.md │ ├── index.md │ └── man_pages │ ├── airup.md │ └── index.md └── extensions └── fallback-logger ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /build_manifest.json 4 | /.DS_Store 5 | /.vscode 6 | /.idea 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor's Manual 2 | Welcome to contribute to Airup! 3 | 4 | ## Developers' Guide 5 | 6 | ## HOWTO: Port Airup To A New Platform 7 | Airup depends on some libraries which contains platform-specific code. All these dependencies must be satisfied: 8 | - `tokio` 9 | - `sysinfo` 10 | 11 | Some OS features are used: 12 | - Unix Domain Sockets 13 | 14 | All other platform-specific codes are wrapped by `airupfx::sys`. 15 | 16 | ## Licensing 17 | By contributing, you agree to license your code under the same license as existing source code 18 | of `Airup`. See [the LICENSE file](LICENSE). 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "airup-sdk", 4 | "airupd", 5 | "airup", 6 | "airupfx/airupfx", 7 | "airupfx/airupfx-io", 8 | "airupfx/airupfx-env", 9 | "airupfx/airupfx-fs", 10 | "airupfx/airupfx-process", 11 | "airupfx/airupfx-power", 12 | "airupfx/airupfx-signal", 13 | "airupfx/airupfx-time", 14 | "airupfx/airupfx-macros", 15 | "airupfx/airupfx-isolator", 16 | "airupfx/airupfx-extensions", 17 | "extensions/fallback-logger", 18 | ] 19 | resolver = "2" 20 | 21 | [profile.release] 22 | codegen-units = 1 23 | lto = true 24 | 25 | [workspace.dependencies.tokio] 26 | version = "1" 27 | features = ["fs", "sync", "net", "io-util", "signal", "rt", "time", "macros", "process"] 28 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Airup Installation Guide 2 | This guide describes how to build, configure and install Airup. 3 | 4 | ## Platform Support 5 | Airup compiles on all Unix-like operating systems. As a service supervisor, it has been tested to work on Linux, macOS, 6 | Android and FreeBSD. As an init system \(`pid == 1`\), it has been tested to work on Linux. 7 | 8 | ## Build Dependencies 9 | To build Airup, you need to install the few dependencies first: 10 | - [Rust](https://rust-lang.org): The programming language used to implement Airup. 11 | 12 | Airup requires `Rust 1.85.0` or newer to compile. 13 | 14 | ## Configuration 15 | Some Airup functions are configured at build time. The build manifest which is located at `build_manifest.json` stores primitive 16 | paths that cannot be set at runtime or default values of `system.conf` items. An example file is located 17 | at `docs/resources/build_manifest.json`. There is a list that describes all available configuration items: 18 | - `os_name`: Name of the OS build. 19 | - `config_dir`: Path of Airup's configuration directory, which stores service/system config files, etc. 20 | - `service_dir`: Path of Airup's service directory, which stores service files. 21 | - `milestone_dir`: Path of Airup's milestone directory, which stores milestone files. 22 | - `runtime_dir`: Path of Airup's runtime directory, which stores runtime files like the Unix sockets, locks, etc. 23 | - `env_vars`: Environment variables for the global system. When a value is explictly set to `null`, the variables is deleted if 24 | it exists. 25 | - `early_cmds`: Commands that are executed in the `early_boot` pseudo-milestone. 26 | 27 | ## Build 28 | To build debug version of Airup, run: 29 | ```shell 30 | cargo build 31 | ``` 32 | 33 | To build release version of Airup, run: 34 | ```shell 35 | cargo build --release 36 | ``` 37 | 38 | ## Install 39 | A standard Airup installation consists of the following files: 40 | - `airupd`: The main Airup daemon binary. 41 | - `airup`: A CLI utility to inspect or manipulate Airup components. 42 | - \[`fallback-logger`\]: An Airup extension that implements a simple logger for the Airup Logger Interface for fallback use. 43 | This is not subject to be executed directly by the user and is usually placed at `/usr/libexec/airup/fallback-logger`. 44 | - `libairup_sdk.so` OR `libairup_sdk.dylib`: The Airup SDK for C, in dynamic library. 45 | - \[`docs/resources/airup-fallback-logger.airs`\]: Service manifest file for the `fallback-logger` service. 46 | - \[`docs/resources/airupd.airs`\]: Stub service manifest file for the `airupd` service. 47 | - \[`docs/resources/selinux/airup.te`\]: SELinux policy for Airup. 48 | 49 | Read the [documents](docs/README.md) to learn more about installation. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2023 sisungo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | and associated documentation files (the “Software”), to deal in the Software without 5 | restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airup 2 | ![Airup Logo](docs/artwork/airup_logo_320x200.png) 3 | 4 | ![License Badge](https://img.shields.io/badge/license-MIT-blue) 5 | 6 | Airup is a modern, portable and fast implementation of service supervisors, and can be used as either a standalone service 7 | supervisor or the `init` daemon (`pid == 1`). 8 | 9 | ## Documentation 10 | [Multilingua documentation](docs/README.md) can be found in [docs/](docs/) directory. 11 | 12 | ## Build & Installation 13 | See [Installation Guide](INSTALL.md). 14 | 15 | ## License 16 | Airup is licensed under the [MIT License](LICENSE). 17 | 18 | ## Contributing 19 | See [Contributor's Manual](CONTRIBUTING.md). 20 | 21 | ## Reporting Security Issues 22 | See [Security Policy](SECURITY.md). 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | The `HEAD` and the latest release are supported by the Airup developers. The maintained versions are: 5 | - Mainline: `0.10.8` 6 | - Stable: `0.10.7` 7 | 8 | ## Reporting a Vulnerability 9 | Please [contact @sisungo](mailto:sisungo@icloud.com) to report a vulnerability. 10 | -------------------------------------------------------------------------------- /airup-sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airup-sdk" 3 | authors = ["sisungo "] 4 | description = "SDK library of Airup" 5 | documentation = "https://docs.rs/airup-sdk" 6 | repository = "https://github.com/sisungo/airup" 7 | version = "0.10.8" 8 | edition = "2024" 9 | license = "MIT" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | crate-type = ["rlib", "cdylib"] 15 | 16 | [features] 17 | _internal = ["tokio-1", "blocking", "ffi"] 18 | ffi = ["blocking"] 19 | blocking = [] 20 | nonblocking = [] 21 | tokio-1 = ["dep:tokio", "nonblocking"] 22 | 23 | [dependencies] 24 | cfg-if = "1" 25 | ciborium = "0.2" 26 | libc = "0.2" 27 | serde = { version = "1", features = ["derive"] } 28 | thiserror = "2" 29 | tokio = { version = "1", features = ["net", "fs", "io-util"], optional = true } 30 | toml = "0.8" 31 | 32 | [build-dependencies] 33 | ciborium = "0.2" 34 | serde_json = "1" 35 | -------------------------------------------------------------------------------- /airup-sdk/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "_internal")] 2 | fn main() { 3 | use std::path::Path; 4 | 5 | println!("cargo::rerun-if-changed=../build_manifest.json"); 6 | let out_dir = std::env::var("OUT_DIR").unwrap(); 7 | let build_manifest: ciborium::Value = 8 | serde_json::from_reader(std::fs::File::open("../build_manifest.json").unwrap()).unwrap(); 9 | let mut file = std::fs::File::options() 10 | .create(true) 11 | .truncate(true) 12 | .write(true) 13 | .open(Path::new(&out_dir).join("build_manifest.cbor")) 14 | .unwrap(); 15 | 16 | ciborium::into_writer(&build_manifest, &mut file).unwrap(); 17 | } 18 | 19 | #[cfg(not(feature = "_internal"))] 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /airup-sdk/src/blocking/files/milestone.rs: -------------------------------------------------------------------------------- 1 | use crate::blocking::fs::DirChain; 2 | use crate::files::{Milestone, ReadError, milestone}; 3 | use std::path::Path; 4 | 5 | pub trait MilestoneExt { 6 | fn read_from>(path: P) -> Result; 7 | fn items(&self) -> Vec; 8 | } 9 | impl MilestoneExt for Milestone { 10 | fn read_from>(path: P) -> Result { 11 | read_from(path.as_ref()) 12 | } 13 | 14 | fn items(&self) -> Vec { 15 | let mut services = Vec::new(); 16 | let chain = DirChain::new(&self.base_dir); 17 | 18 | let Ok(read_chain) = chain.read_chain() else { 19 | return services; 20 | }; 21 | 22 | for i in read_chain { 23 | if !i.to_string_lossy().ends_with(".list.airf") { 24 | continue; 25 | } 26 | let Some(path) = chain.find(&i) else { 27 | continue; 28 | }; 29 | let Ok(list_str) = std::fs::read_to_string(&path) else { 30 | continue; 31 | }; 32 | for line in list_str.lines() { 33 | if let Ok(item) = line.parse() { 34 | services.push(item); 35 | } 36 | } 37 | } 38 | 39 | services 40 | } 41 | } 42 | 43 | fn read_from(path: &Path) -> Result { 44 | let get_name = |p: &Path| -> Result { 45 | Ok(p.file_stem() 46 | .ok_or_else(|| ReadError::from("invalid milestone path"))? 47 | .to_string_lossy() 48 | .into()) 49 | }; 50 | let manifest = toml::from_str(&std::fs::read_to_string( 51 | path.join(milestone::Manifest::FILE_NAME), 52 | )?)?; 53 | let mut name = get_name(path)?; 54 | if name == "default" { 55 | name = get_name(&std::fs::canonicalize(path)?)?; 56 | } 57 | 58 | Ok(Milestone { 59 | name, 60 | manifest, 61 | base_dir: path.into(), 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /airup-sdk/src/blocking/files/mod.rs: -------------------------------------------------------------------------------- 1 | mod milestone; 2 | mod system_conf; 3 | 4 | pub use milestone::MilestoneExt; 5 | pub use system_conf::SystemConfExt; 6 | 7 | use crate::files::{Named, ReadError, Validate}; 8 | use serde::de::DeserializeOwned; 9 | use std::path::PathBuf; 10 | 11 | pub fn read_merge( 12 | paths: Vec, 13 | ) -> Result { 14 | let Some(main_path) = paths.first() else { 15 | panic!("parameter `paths` must not be empty"); 16 | }; 17 | let main = std::fs::read_to_string(main_path)?; 18 | let mut main = toml::from_str(&main)?; 19 | 20 | for path in &paths[1..] { 21 | let content = std::fs::read_to_string(path)?; 22 | let patch = toml::from_str(&content)?; 23 | crate::files::merge(&mut main, &patch); 24 | } 25 | 26 | let mut object: T = T::deserialize(main)?; 27 | 28 | object.validate()?; 29 | object.set_name(main_path.file_stem().unwrap().to_string_lossy().into()); 30 | 31 | Ok(object) 32 | } 33 | -------------------------------------------------------------------------------- /airup-sdk/src/blocking/files/system_conf.rs: -------------------------------------------------------------------------------- 1 | use crate::files::{ReadError, SystemConf}; 2 | use std::path::Path; 3 | 4 | pub trait SystemConfExt { 5 | fn read_from>(path: P) -> Result; 6 | } 7 | impl SystemConfExt for SystemConf { 8 | fn read_from>(path: P) -> Result { 9 | read_from(path.as_ref()) 10 | } 11 | } 12 | 13 | fn read_from(path: &Path) -> Result { 14 | let s = std::fs::read_to_string(path)?; 15 | Ok(toml::from_str(&s)?) 16 | } 17 | -------------------------------------------------------------------------------- /airup-sdk/src/blocking/fs.rs: -------------------------------------------------------------------------------- 1 | //! Filesystem utilities. 2 | 3 | use crate::util::IterExt; 4 | use std::{ 5 | borrow::Cow, 6 | collections::HashSet, 7 | ffi::OsString, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | /// Represents to a "directory chain", which has a filesystem layout similar to: 12 | /// ```text 13 | /// /dir_chain 14 | /// /file1.txt 15 | /// /file2.txt 16 | /// /chain_next -> /dir_chain1 17 | /// /dir_chain1 18 | /// /file1.txt 19 | /// /file3.txt 20 | /// /file4.txt 21 | /// ... 22 | /// ``` 23 | /// When finding a file or directory from the chain, the program will iterate over each directory in the chain, until the 24 | /// matching file or directory is found. For example, in the chain above, finding `file1.txt` returns `/dir_chain/file1.txt`, 25 | /// and finding `file3.txt` returns `/dir_chain1/file3.txt`. 26 | #[derive(Debug, Clone)] 27 | pub struct DirChain<'a>(Cow<'a, Path>); 28 | impl<'a> DirChain<'a> { 29 | pub fn new>>(path: P) -> Self { 30 | Self(path.into()) 31 | } 32 | 33 | /// Find a file by filename. 34 | pub fn find>(&self, path: P) -> Option { 35 | let mut pwd = self.0.clone(); 36 | let path = path.as_ref(); 37 | 38 | loop { 39 | let path = pwd.join(path); 40 | if path.exists() { 41 | return Some(path); 42 | } else { 43 | let path = pwd.join("chain_next"); 44 | if path.exists() { 45 | pwd = path.into(); 46 | } else { 47 | return None; 48 | } 49 | } 50 | } 51 | } 52 | 53 | /// Returns path of end of the chain. 54 | pub fn end(&self) -> PathBuf { 55 | let mut pwd = self.0.clone(); 56 | 57 | loop { 58 | let chain_next = pwd.join("chain_next"); 59 | if chain_next.exists() { 60 | pwd = chain_next.into(); 61 | } else { 62 | break pwd.into(); 63 | } 64 | } 65 | } 66 | 67 | /// Gets a list that contains relative paths of filesystem objects on the chain. The result is sorted (chain-order first). 68 | /// 69 | /// # Errors 70 | /// An `Err(_)` is returned if the underlying filesystem operation failed. 71 | pub fn read_chain(&self) -> std::io::Result> { 72 | let mut result = Vec::new(); 73 | let mut pwd = self.0.clone(); 74 | let mut elements = HashSet::new(); 75 | let mut unsorted = Vec::new(); 76 | 77 | loop { 78 | let mut should_continue = false; 79 | 80 | let mut read_dir = std::fs::read_dir(&pwd)?; 81 | while let Some(Ok(entry)) = read_dir.next() { 82 | let file_name = entry.file_name(); 83 | if file_name == "chain_next" { 84 | should_continue = true; 85 | } else { 86 | elements.insert(file_name); 87 | } 88 | } 89 | 90 | elements.drain().for_each(|x| unsorted.push(x)); 91 | unsorted.sort_unstable(); 92 | result.append(&mut unsorted); 93 | 94 | if should_continue { 95 | pwd = pwd.join("chain_next").into(); 96 | } else { 97 | break; 98 | } 99 | } 100 | 101 | Ok(result.into_iter().dedup_all()) 102 | } 103 | 104 | /// Finds a file from the chain, or creates it at the end of the chain if not found. 105 | /// 106 | /// # Errors 107 | /// An `Err(_)` is returned if the underlying filesystem operation failed. 108 | pub fn find_or_create>(&self, path: P) -> std::io::Result { 109 | let path = path.as_ref(); 110 | if let Some(np) = self.find(path) { 111 | Ok(np) 112 | } else { 113 | let np = self.end().join(path); 114 | std::fs::File::create(&np)?; 115 | Ok(np) 116 | } 117 | } 118 | } 119 | impl From for DirChain<'static> { 120 | fn from(value: PathBuf) -> Self { 121 | Self::new(value) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /airup-sdk/src/blocking/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod files; 2 | pub mod fs; 3 | pub mod rpc; 4 | 5 | use crate::{ 6 | error::ApiError, 7 | rpc::{Error as IpcError, Request}, 8 | }; 9 | use rpc::{MessageProtoRecvExt, MessageProtoSendExt}; 10 | use serde::{Serialize, de::DeserializeOwned}; 11 | use std::{ 12 | ops::{Deref, DerefMut}, 13 | path::Path, 14 | }; 15 | 16 | /// A high-level wrapper of a connection to `airupd`. 17 | #[derive(Debug)] 18 | pub struct Connection { 19 | underlying: rpc::Connection, 20 | } 21 | impl Connection { 22 | /// Connects to the specific path. 23 | pub fn connect>(path: P) -> std::io::Result { 24 | Ok(Self { 25 | underlying: rpc::Connection::connect(path)?, 26 | }) 27 | } 28 | 29 | /// Sends a raw message. 30 | pub fn send_raw(&mut self, msg: &[u8]) -> Result<(), IpcError> { 31 | (*self.underlying).send(msg) 32 | } 33 | 34 | /// Receives a raw message. 35 | pub fn recv_raw(&mut self) -> Result, IpcError> { 36 | let mut buf = Vec::new(); 37 | (*self.underlying).recv(&mut buf)?; 38 | Ok(buf) 39 | } 40 | 41 | /// Invokes an RPC method. 42 | pub fn invoke( 43 | &mut self, 44 | method: &str, 45 | params: P, 46 | ) -> Result, IpcError> { 47 | let req = Request::new(method, params); 48 | self.underlying.send(&req)?; 49 | Ok(self 50 | .underlying 51 | .recv::()? 52 | .into_result()) 53 | } 54 | 55 | pub fn into_inner(self) -> rpc::Connection { 56 | self.underlying 57 | } 58 | } 59 | impl Deref for Connection { 60 | type Target = rpc::Connection; 61 | 62 | fn deref(&self) -> &Self::Target { 63 | &self.underlying 64 | } 65 | } 66 | impl DerefMut for Connection { 67 | fn deref_mut(&mut self) -> &mut Self::Target { 68 | &mut self.underlying 69 | } 70 | } 71 | 72 | impl crate::Connection for Connection { 73 | type Invoke<'a, T: 'a> = Result, IpcError>; 74 | 75 | fn invoke<'a, P: Serialize + 'a, T: DeserializeOwned + 'a>( 76 | &'a mut self, 77 | method: &'a str, 78 | params: P, 79 | ) -> Self::Invoke<'a, T> { 80 | self.invoke(method, params) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /airup-sdk/src/blocking/rpc.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ApiError, 3 | rpc::{Error as IpcError, MessageProto, Request, Response}, 4 | }; 5 | use serde::{Serialize, de::DeserializeOwned}; 6 | use std::{ 7 | io::{Read, Write}, 8 | ops::{Deref, DerefMut}, 9 | os::unix::net::UnixStream, 10 | path::Path, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct Connection(MessageProto); 15 | impl Connection { 16 | /// Connects to the specified socket. 17 | pub fn connect>(path: P) -> std::io::Result { 18 | Ok(Self(MessageProto::new( 19 | UnixStream::connect(path)?, 20 | usize::MAX, 21 | ))) 22 | } 23 | 24 | /// Receives a datagram and deserializes it from CBOR to `T`. 25 | pub fn recv(&mut self) -> Result { 26 | let mut buf = Vec::new(); 27 | self.0.recv(&mut buf)?; 28 | Ok(ciborium::from_reader(&buf[..])?) 29 | } 30 | 31 | /// Receives a request from the underlying protocol. 32 | pub fn recv_req(&mut self) -> Result { 33 | let mut buf = Vec::new(); 34 | self.0.recv(&mut buf)?; 35 | let req: Request = ciborium::from_reader(&buf[..]).unwrap_or_else(|err| { 36 | Request::new( 37 | "debug.echo_raw", 38 | Response::Err(ApiError::bad_request("InvalidCbor", err.to_string())), 39 | ) 40 | }); 41 | Ok(req) 42 | } 43 | 44 | /// Sends a datagram with CBOR-serialized given object. 45 | pub fn send(&mut self, obj: &T) -> Result<(), IpcError> { 46 | let mut buffer = Vec::with_capacity(128); 47 | ciborium::into_writer(obj, &mut buffer)?; 48 | self.0.send(&buffer) 49 | } 50 | 51 | /// Returns the underlying message protocol. 52 | pub fn into_inner(self) -> MessageProto { 53 | self.0 54 | } 55 | } 56 | impl Deref for Connection { 57 | type Target = MessageProto; 58 | 59 | fn deref(&self) -> &Self::Target { 60 | &self.0 61 | } 62 | } 63 | impl DerefMut for Connection { 64 | fn deref_mut(&mut self) -> &mut Self::Target { 65 | &mut self.0 66 | } 67 | } 68 | 69 | pub trait MessageProtoRecvExt { 70 | /// Receives a message from the stream. 71 | fn recv(&mut self, buf: &mut Vec) -> Result<(), IpcError>; 72 | } 73 | pub trait MessageProtoSendExt { 74 | /// Sends a message to the stream 75 | fn send(&mut self, blob: &[u8]) -> Result<(), IpcError>; 76 | } 77 | impl MessageProtoRecvExt for MessageProto { 78 | fn recv(&mut self, buf: &mut Vec) -> Result<(), IpcError> { 79 | let mut len = [0u8; 8]; 80 | self.inner.read_exact(&mut len)?; 81 | let len = u64::from_le_bytes(len) as usize; 82 | if len > self.size_limit { 83 | return Err(IpcError::MessageTooLong(len)); 84 | } 85 | buf.resize(len, 0u8); 86 | self.inner.read_exact(buf)?; 87 | Ok(()) 88 | } 89 | } 90 | impl MessageProtoSendExt for MessageProto { 91 | fn send(&mut self, blob: &[u8]) -> Result<(), IpcError> { 92 | self.inner.write_all(&u64::to_le_bytes(blob.len() as _))?; 93 | self.inner.write_all(blob)?; 94 | 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /airup-sdk/src/build.rs: -------------------------------------------------------------------------------- 1 | //! Information about an Airup build. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; 5 | 6 | static MANIFEST: OnceLock = OnceLock::new(); 7 | 8 | /// Represents to the structure of the build manifest, which is usually read from `build_manifest.json` at compile-time. 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | pub struct BuildManifest { 11 | /// Name of the running operating system. 12 | #[serde(default = "default_os_name")] 13 | pub os_name: String, 14 | 15 | /// Path of Airup's config directory, e.g. `/etc/airup`. 16 | pub config_dir: PathBuf, 17 | 18 | /// Path of Airup's service directory, e.g. `/etc/airup/services`. 19 | pub service_dir: PathBuf, 20 | 21 | /// Path of Airup's milestone directory, e.g. `/etc/airup/milestones`. 22 | pub milestone_dir: PathBuf, 23 | 24 | /// Path of Airup's runtime directory, e.g. `/run/airup`. 25 | pub runtime_dir: PathBuf, 26 | 27 | /// Table of initial environment variables. 28 | #[serde(default)] 29 | pub env_vars: HashMap>, 30 | 31 | /// Commands executed in `early_boot` pseudo-milestone. 32 | #[serde(default)] 33 | pub early_cmds: Vec, 34 | 35 | /// Name of Airup's socket in the abstract namespace. This is Linux-only. 36 | #[cfg(target_os = "linux")] 37 | pub linux_ipc_name: Option, 38 | } 39 | 40 | fn default_os_name() -> String { 41 | "\x1b[36;4mAirup\x1b[0m".into() 42 | } 43 | 44 | /// Gets a reference to the global [`BuildManifest`] instance. If [`set_manifest`] was not previously called, it automatically 45 | /// initializes the instance by reading the compile-time `build_manifest.json`. 46 | /// 47 | /// # Panics 48 | /// Panics if the [`BuildManifest`] instance was not initialized yet and the compile-time `build_manifest.json` was invalid. 49 | pub fn manifest() -> &'static BuildManifest { 50 | #[cfg(feature = "_internal")] 51 | { 52 | MANIFEST.get_or_init(embedded_manifest) 53 | } 54 | 55 | #[cfg(not(feature = "_internal"))] 56 | { 57 | MANIFEST.get().unwrap() 58 | } 59 | } 60 | 61 | /// Sets the build manifest to the specific value. 62 | /// 63 | /// # Panics 64 | /// Panics if the manifest is already set, which may be done by any call of [`manifest`] or [`set_manifest`]. 65 | pub fn set_manifest(manifest: BuildManifest) { 66 | MANIFEST.set(manifest).unwrap(); 67 | } 68 | 69 | /// Sets the build manifest to the specific value. 70 | /// 71 | /// # Panics 72 | /// Panics if the manifest is already set, which may be done by any call of [`manifest`] or [`set_manifest`]. 73 | pub fn try_set_manifest(manifest: BuildManifest) -> Option<()> { 74 | MANIFEST.set(manifest).ok() 75 | } 76 | 77 | /// Returns the embedded [`BuildManifest`] instance. 78 | /// 79 | /// # Panics 80 | /// Panics if the compile-time `build_manifest.json` was invalid. 81 | #[doc(hidden)] 82 | #[cfg(feature = "_internal")] 83 | fn embedded_manifest() -> BuildManifest { 84 | ciborium::from_reader(&include_bytes!(concat!(env!("OUT_DIR"), "/build_manifest.cbor"))[..]) 85 | .expect("bad airup build") 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | #[cfg(feature = "_internal")] 91 | #[test] 92 | fn embedded_manifest() { 93 | super::embedded_manifest(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /airup-sdk/src/debug.rs: -------------------------------------------------------------------------------- 1 | /// An extension trait to provide `debug.*` API invocation. 2 | pub trait ConnectionExt<'a>: crate::Connection { 3 | fn is_forking_supervisable(&'a mut self) -> Self::Invoke<'a, bool> { 4 | self.invoke("debug.is_forking_supervisable", ()) 5 | } 6 | 7 | fn dump(&'a mut self) -> Self::Invoke<'a, String> { 8 | self.invoke("debug.dump", ()) 9 | } 10 | } 11 | impl ConnectionExt<'_> for T where T: crate::Connection {} 12 | -------------------------------------------------------------------------------- /airup-sdk/src/extapi.rs: -------------------------------------------------------------------------------- 1 | use crate::system::LogRecord; 2 | 3 | /// An extension trait to provide invocation for conventional `extapi.*` APIs. 4 | pub trait ConnectionExt<'a>: crate::Connection { 5 | fn append_log( 6 | &'a mut self, 7 | subject: &'a str, 8 | module: &'a str, 9 | msg: &'a [u8], 10 | ) -> Self::Invoke<'a, ()> { 11 | self.invoke("extapi.logger.append", (subject, module, msg)) 12 | } 13 | 14 | fn tail_logs(&'a mut self, subject: &'a str, n: usize) -> Self::Invoke<'a, Vec> { 15 | self.invoke("extapi.logger.tail", (subject, n)) 16 | } 17 | } 18 | impl ConnectionExt<'_> for T where T: crate::Connection {} 19 | -------------------------------------------------------------------------------- /airup-sdk/src/extension/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct Request { 5 | pub id: u64, 6 | pub class: u8, 7 | pub data: ciborium::Value, 8 | } 9 | impl Request { 10 | pub const CLASS_AIRUP_RPC: u8 = 1; 11 | } 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct Response { 15 | pub id: u64, 16 | pub data: ciborium::Value, 17 | } 18 | -------------------------------------------------------------------------------- /airup-sdk/src/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /airup-sdk/src/files/milestone.rs: -------------------------------------------------------------------------------- 1 | //! # Milestones 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::{path::PathBuf, str::FromStr}; 5 | 6 | /// Represents to an Airup milestone. 7 | #[derive(Debug, Clone)] 8 | pub struct Milestone { 9 | pub name: String, 10 | pub manifest: Manifest, 11 | pub base_dir: PathBuf, 12 | } 13 | impl Milestone { 14 | /// Returns the name to display for this service. 15 | pub fn display_name(&self) -> &str { 16 | self.manifest 17 | .milestone 18 | .display_name 19 | .as_deref() 20 | .unwrap_or(&self.name) 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize, Deserialize)] 25 | pub struct Manifest { 26 | pub milestone: Metadata, 27 | } 28 | impl Manifest { 29 | pub const FILE_NAME: &'static str = "milestone.airf"; 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | #[serde(rename_all = "kebab-case")] 34 | pub struct Metadata { 35 | /// Display of the milestone. 36 | pub display_name: Option, 37 | 38 | /// Description of the milestone. 39 | pub description: Option, 40 | 41 | /// Dependencies of the milestone. 42 | #[serde(default)] 43 | pub dependencies: Vec, 44 | 45 | /// Tags of the milestone. 46 | #[serde(default)] 47 | pub tags: Vec, 48 | 49 | #[serde(default)] 50 | pub kind: Kind, 51 | } 52 | 53 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 54 | #[serde(rename_all = "kebab-case")] 55 | pub enum Kind { 56 | /// Services are asynchronously started. 57 | #[default] 58 | Async, 59 | 60 | /// Services are asynchronously started, but the milestone is completed when they are active. 61 | Sync, 62 | 63 | /// Latter services must be executed after the former service is active. 64 | Serial, 65 | } 66 | 67 | #[derive(Debug, Clone)] 68 | pub enum Item { 69 | Cache(String), 70 | Start(String), 71 | Run(String), 72 | } 73 | impl FromStr for Item { 74 | type Err = super::ReadError; 75 | 76 | fn from_str(s: &str) -> Result { 77 | let mut splited = s.splitn(2, ' '); 78 | let verb = splited 79 | .next() 80 | .ok_or_else(|| super::ReadError::from("missing verb in milestone items"))?; 81 | let entity = splited 82 | .next() 83 | .ok_or_else(|| super::ReadError::from("missing object in milestone items"))?; 84 | match verb { 85 | "cache" => Ok(Self::Cache(entity.into())), 86 | "start" => Ok(Self::Start(entity.into())), 87 | "run" => Ok(Self::Run(entity.into())), 88 | _ => Err("verb `{verb}` is not considered in milestone items".into()), 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /airup-sdk/src/files/mod.rs: -------------------------------------------------------------------------------- 1 | //! Definitions of Airup's file formats. 2 | 3 | pub mod milestone; 4 | pub mod service; 5 | pub mod system_conf; 6 | 7 | pub use milestone::Milestone; 8 | pub use service::Service; 9 | pub use system_conf::SystemConf; 10 | 11 | use crate::prelude::*; 12 | use std::{borrow::Cow, sync::Arc}; 13 | 14 | pub trait Validate { 15 | fn validate(&self) -> Result<(), ReadError>; 16 | } 17 | 18 | pub trait Named { 19 | fn set_name(&mut self, name: String); 20 | } 21 | 22 | pub fn merge(doc: &mut toml::Value, patch: &toml::Value) { 23 | if !patch.is_table() { 24 | *doc = patch.clone(); 25 | return; 26 | } 27 | 28 | if !doc.is_table() { 29 | *doc = toml::Value::Table(toml::Table::new()); 30 | } 31 | let map = doc.as_table_mut().unwrap(); 32 | for (key, value) in patch.as_table().unwrap() { 33 | if value.is_table() && value.as_table().unwrap().is_empty() { 34 | map.remove(key.as_str()); 35 | } else { 36 | merge( 37 | map.entry(key.as_str()) 38 | .or_insert(toml::Value::Table(toml::Table::new())), 39 | value, 40 | ); 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone, thiserror::Error)] 46 | pub enum ReadError { 47 | #[error("{0}")] 48 | Io(Arc), 49 | 50 | #[error("{0}")] 51 | Parse(String), 52 | 53 | #[error("{0}")] 54 | Validation(Cow<'static, str>), 55 | } 56 | impl From for ReadError { 57 | fn from(value: std::io::Error) -> Self { 58 | Self::Io(value.into()) 59 | } 60 | } 61 | impl From for ReadError { 62 | fn from(value: toml::de::Error) -> Self { 63 | Self::Parse(value.message().to_owned()) 64 | } 65 | } 66 | impl From<&'static str> for ReadError { 67 | fn from(value: &'static str) -> Self { 68 | Self::Validation(value.into()) 69 | } 70 | } 71 | impl From for ReadError { 72 | fn from(value: String) -> Self { 73 | Self::Validation(value.into()) 74 | } 75 | } 76 | impl From for ReadError { 77 | fn from(value: std::io::ErrorKind) -> Self { 78 | Self::from(std::io::Error::from(value)) 79 | } 80 | } 81 | impl IntoApiError for ReadError { 82 | fn into_api_error(self) -> crate::Error { 83 | match self { 84 | Self::Io(err) => match err.kind() { 85 | std::io::ErrorKind::NotFound => crate::Error::NotFound, 86 | _ => crate::Error::Io { 87 | message: err.to_string(), 88 | }, 89 | }, 90 | Self::Parse(x) => crate::Error::BadObject { message: x.into() }, 91 | Self::Validation(x) => crate::Error::BadObject { message: x }, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /airup-sdk/src/files/system_conf.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | /// Representation of Airup's system config. 5 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "kebab-case")] 7 | pub struct SystemConf { 8 | #[serde(default)] 9 | pub system: System, 10 | 11 | #[serde(default)] 12 | pub env: Env, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | #[serde(rename_all = "kebab-case")] 17 | pub struct System { 18 | #[serde(default = "default_os_name")] 19 | pub os_name: String, 20 | 21 | #[serde(default = "default_reboot_timeout")] 22 | pub reboot_timeout: u32, 23 | 24 | #[serde(default)] 25 | pub instance_name: String, 26 | } 27 | impl Default for System { 28 | fn default() -> Self { 29 | Self { 30 | os_name: default_os_name(), 31 | reboot_timeout: default_reboot_timeout(), 32 | instance_name: String::new(), 33 | } 34 | } 35 | } 36 | 37 | /// Represents to Airup's environment. 38 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 39 | pub struct Env { 40 | /// Table of initial environment variables. 41 | /// 42 | /// If a value is set to `null`, the environment variable gets removed if it exists. 43 | #[serde(default)] 44 | pub vars: HashMap, 45 | } 46 | 47 | fn default_os_name() -> String { 48 | crate::build::manifest().os_name.clone() 49 | } 50 | 51 | fn default_reboot_timeout() -> u32 { 52 | 1200000 53 | } 54 | -------------------------------------------------------------------------------- /airup-sdk/src/info.rs: -------------------------------------------------------------------------------- 1 | use crate::build::BuildManifest; 2 | 3 | /// An extension trait to provide `info.*` API invocation. 4 | pub trait ConnectionExt<'a>: crate::Connection { 5 | fn build_manifest(&'a mut self) -> Self::Invoke<'a, BuildManifest> { 6 | self.invoke("info.build_manifest", ()) 7 | } 8 | 9 | fn version(&'a mut self) -> Self::Invoke<'a, String> { 10 | self.invoke("info.version", ()) 11 | } 12 | } 13 | impl ConnectionExt<'_> for T where T: crate::Connection {} 14 | -------------------------------------------------------------------------------- /airup-sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # The Airup SDK 2 | //! The Airup SDK provides interface to access Airup facilities, for example, interacting with the daemon, `airupd`. 3 | 4 | pub mod build; 5 | pub mod debug; 6 | pub mod error; 7 | pub mod extapi; 8 | pub mod extension; 9 | pub mod files; 10 | pub mod info; 11 | pub mod prelude; 12 | pub mod rpc; 13 | pub mod system; 14 | 15 | mod util; 16 | 17 | #[cfg(feature = "nonblocking")] 18 | pub mod nonblocking; 19 | 20 | #[cfg(feature = "blocking")] 21 | pub mod blocking; 22 | 23 | #[cfg(feature = "ffi")] 24 | pub mod ffi; 25 | 26 | pub use error::ApiError as Error; 27 | 28 | use serde::{Serialize, de::DeserializeOwned}; 29 | use std::{ 30 | path::{Path, PathBuf}, 31 | sync::OnceLock, 32 | }; 33 | 34 | /// Returns default path of Airup's IPC socket. 35 | /// 36 | /// If environment `AIRUP_SOCK` was present, returns the value of `AIRUP_SOCK`. Otherwise it returns `$runtime_dir/airupd.sock`, 37 | /// which is related to the compile-time `build_manifest.json`. 38 | pub fn socket_path() -> &'static Path { 39 | static SOCKET_PATH: OnceLock<&'static Path> = OnceLock::new(); 40 | 41 | SOCKET_PATH.get_or_init(|| { 42 | Box::leak( 43 | std::env::var("AIRUP_SOCK") 44 | .map(PathBuf::from) 45 | .unwrap_or_else(|_| build::manifest().runtime_dir.join("airupd.sock")) 46 | .into(), 47 | ) 48 | }) 49 | } 50 | 51 | /// A trait that unifies `async` and `non-async` connections. 52 | pub trait Connection { 53 | /// Return type of the [`Connection::invoke`] method. 54 | type Invoke<'a, T: 'a> 55 | where 56 | Self: 'a; 57 | 58 | /// Invokes specified method with given parameters on the connection, then wait for a response. 59 | fn invoke<'a, P: Serialize + Send + 'a, T: DeserializeOwned + 'a>( 60 | &'a mut self, 61 | method: &'a str, 62 | params: P, 63 | ) -> Self::Invoke<'a, T>; 64 | } 65 | -------------------------------------------------------------------------------- /airup-sdk/src/nonblocking/files/milestone.rs: -------------------------------------------------------------------------------- 1 | use crate::files::{Milestone, ReadError, milestone}; 2 | use crate::nonblocking::fs::DirChain; 3 | use std::{future::Future, path::Path}; 4 | 5 | pub trait MilestoneExt { 6 | fn read_from>(path: P) -> impl Future>; 7 | fn items(&self) -> impl Future>; 8 | } 9 | impl MilestoneExt for Milestone { 10 | async fn read_from>(path: P) -> Result { 11 | read_from(path.as_ref()).await 12 | } 13 | 14 | async fn items(&self) -> Vec { 15 | let mut services = Vec::new(); 16 | let chain = DirChain::new(&self.base_dir); 17 | 18 | let Ok(read_chain) = chain.read_chain().await else { 19 | return services; 20 | }; 21 | 22 | for i in read_chain { 23 | if !i.to_string_lossy().ends_with(".list.airf") { 24 | continue; 25 | } 26 | let Some(path) = chain.find(&i).await else { 27 | continue; 28 | }; 29 | let Ok(list_str) = tokio::fs::read_to_string(&path).await else { 30 | continue; 31 | }; 32 | for line in list_str.lines() { 33 | if let Ok(item) = line.parse() { 34 | services.push(item); 35 | } 36 | } 37 | } 38 | 39 | services 40 | } 41 | } 42 | 43 | async fn read_from(path: &Path) -> Result { 44 | let get_name = |p: &Path| -> Result { 45 | Ok(p.file_stem() 46 | .ok_or_else(|| ReadError::from("invalid milestone path"))? 47 | .to_string_lossy() 48 | .into()) 49 | }; 50 | let manifest = toml::from_str( 51 | &tokio::fs::read_to_string(path.join(milestone::Manifest::FILE_NAME)).await?, 52 | )?; 53 | let mut name = get_name(path)?; 54 | if name == "default" { 55 | name = get_name(&tokio::fs::canonicalize(path).await?)?; 56 | } 57 | 58 | Ok(Milestone { 59 | name, 60 | manifest, 61 | base_dir: path.into(), 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /airup-sdk/src/nonblocking/files/mod.rs: -------------------------------------------------------------------------------- 1 | mod milestone; 2 | mod system_conf; 3 | 4 | pub use milestone::MilestoneExt; 5 | pub use system_conf::SystemConfExt; 6 | 7 | use crate::files::{Named, ReadError, Validate}; 8 | use serde::de::DeserializeOwned; 9 | use std::path::PathBuf; 10 | 11 | pub async fn read_merge( 12 | paths: Vec, 13 | ) -> Result { 14 | let Some(main_path) = paths.first() else { 15 | panic!("parameter `paths` must not be empty"); 16 | }; 17 | let main = tokio::fs::read_to_string(main_path).await?; 18 | let mut main = toml::from_str(&main)?; 19 | 20 | for path in &paths[1..] { 21 | let content = tokio::fs::read_to_string(path).await?; 22 | let patch = toml::from_str(&content)?; 23 | crate::files::merge(&mut main, &patch); 24 | } 25 | 26 | let mut object: T = T::deserialize(main)?; 27 | 28 | object.validate()?; 29 | object.set_name(main_path.file_stem().unwrap().to_string_lossy().into()); 30 | 31 | Ok(object) 32 | } 33 | -------------------------------------------------------------------------------- /airup-sdk/src/nonblocking/files/system_conf.rs: -------------------------------------------------------------------------------- 1 | use crate::files::{ReadError, SystemConf}; 2 | use std::{future::Future, path::Path}; 3 | 4 | pub trait SystemConfExt { 5 | fn read_from>(path: P) -> impl Future>; 6 | } 7 | impl SystemConfExt for SystemConf { 8 | async fn read_from>(path: P) -> Result { 9 | read_from(path.as_ref()).await 10 | } 11 | } 12 | 13 | async fn read_from(path: &Path) -> Result { 14 | let s = tokio::fs::read_to_string(path).await?; 15 | Ok(toml::from_str(&s)?) 16 | } 17 | -------------------------------------------------------------------------------- /airup-sdk/src/nonblocking/fs.rs: -------------------------------------------------------------------------------- 1 | //! Filesystem utilities. 2 | 3 | use crate::util::IterExt; 4 | use std::{ 5 | borrow::Cow, 6 | collections::HashSet, 7 | ffi::OsString, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | /// Represents to a "directory chain", which has a filesystem layout similar to: 12 | /// ```text 13 | /// /dir_chain 14 | /// /file1.txt 15 | /// /file2.txt 16 | /// /chain_next -> /dir_chain1 17 | /// /dir_chain1 18 | /// /file1.txt 19 | /// /file3.txt 20 | /// /file4.txt 21 | /// ... 22 | /// ``` 23 | /// When finding a file or directory from the chain, the program will iterate over each directory in the chain, until the 24 | /// matching file or directory is found. For example, in the chain above, finding `file1.txt` returns `/dir_chain/file1.txt`, 25 | /// and finding `file3.txt` returns `/dir_chain1/file3.txt`. 26 | #[derive(Debug, Clone)] 27 | pub struct DirChain<'a>(Cow<'a, Path>); 28 | impl<'a> DirChain<'a> { 29 | pub fn new>>(path: P) -> Self { 30 | Self(path.into()) 31 | } 32 | 33 | /// Find a file by filename. 34 | pub async fn find>(&self, path: P) -> Option { 35 | let mut pwd = self.0.clone(); 36 | let path = path.as_ref(); 37 | 38 | loop { 39 | let path = pwd.join(path); 40 | if tokio::fs::try_exists(&path).await.unwrap_or_default() { 41 | return Some(path); 42 | } else { 43 | let path = pwd.join("chain_next"); 44 | if tokio::fs::try_exists(&path).await.unwrap_or_default() { 45 | pwd = path.into(); 46 | } else { 47 | return None; 48 | } 49 | } 50 | } 51 | } 52 | 53 | /// Returns path of end of the chain. 54 | pub async fn end(&self) -> PathBuf { 55 | let mut pwd = self.0.clone(); 56 | 57 | loop { 58 | let chain_next = pwd.join("chain_next"); 59 | if tokio::fs::try_exists(&chain_next).await.unwrap_or_default() { 60 | pwd = chain_next.into(); 61 | } else { 62 | break pwd.into(); 63 | } 64 | } 65 | } 66 | 67 | /// Gets a list that contains relative paths of filesystem objects on the chain. The result is sorted (chain-order first). 68 | /// 69 | /// # Errors 70 | /// An `Err(_)` is returned if the underlying filesystem operation failed. 71 | pub async fn read_chain(&self) -> std::io::Result> { 72 | let mut result = Vec::new(); 73 | let mut pwd = self.0.clone(); 74 | let mut elements = HashSet::new(); 75 | let mut unsorted = Vec::new(); 76 | 77 | loop { 78 | let mut should_continue = false; 79 | 80 | let mut read_dir = tokio::fs::read_dir(&pwd).await?; 81 | while let Ok(Some(entry)) = read_dir.next_entry().await { 82 | let file_name = entry.file_name(); 83 | if file_name == "chain_next" { 84 | should_continue = true; 85 | } else { 86 | elements.insert(file_name); 87 | } 88 | } 89 | 90 | elements.drain().for_each(|x| unsorted.push(x)); 91 | unsorted.sort_unstable(); 92 | result.append(&mut unsorted); 93 | 94 | if should_continue { 95 | pwd = pwd.join("chain_next").into(); 96 | } else { 97 | break; 98 | } 99 | } 100 | 101 | Ok(result.into_iter().dedup_all()) 102 | } 103 | 104 | /// Finds a file from the chain, or creates it at the end of the chain if not found. 105 | /// 106 | /// # Errors 107 | /// An `Err(_)` is returned if the underlying filesystem operation failed. 108 | pub async fn find_or_create>(&self, path: P) -> std::io::Result { 109 | let path = path.as_ref(); 110 | if let Some(np) = self.find(path).await { 111 | Ok(np) 112 | } else { 113 | let np = self.end().await.join(path); 114 | tokio::fs::File::create(&np).await?; 115 | Ok(np) 116 | } 117 | } 118 | } 119 | impl From for DirChain<'static> { 120 | fn from(value: PathBuf) -> Self { 121 | Self::new(value) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /airup-sdk/src/nonblocking/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod files; 2 | pub mod fs; 3 | pub mod rpc; 4 | 5 | use crate::{ 6 | Error as ApiError, 7 | rpc::{Error as IpcError, Request}, 8 | }; 9 | use rpc::{MessageProtoRecvExt, MessageProtoSendExt}; 10 | use serde::{Serialize, de::DeserializeOwned}; 11 | use std::{ 12 | future::Future, 13 | ops::{Deref, DerefMut}, 14 | path::Path, 15 | pin::Pin, 16 | }; 17 | 18 | /// A high-level wrapper of a connection to `airupd`. 19 | #[derive(Debug)] 20 | pub struct Connection { 21 | underlying: rpc::Connection, 22 | } 23 | impl Connection { 24 | /// Connects to the specific path. 25 | pub async fn connect>(path: P) -> std::io::Result { 26 | Ok(Self { 27 | underlying: rpc::Connection::connect(path).await?, 28 | }) 29 | } 30 | 31 | /// Sends a raw message. 32 | pub async fn send_raw(&mut self, msg: &[u8]) -> Result<(), IpcError> { 33 | (*self.underlying).send(msg).await 34 | } 35 | 36 | /// Receives a raw message. 37 | pub async fn recv_raw(&mut self) -> Result, IpcError> { 38 | let mut buf = Vec::new(); 39 | (*self.underlying).recv(&mut buf).await?; 40 | Ok(buf) 41 | } 42 | 43 | /// Invokes an RPC method. 44 | pub async fn invoke( 45 | &mut self, 46 | method: &str, 47 | params: P, 48 | ) -> Result, IpcError> { 49 | let req = Request::new(method, params); 50 | self.underlying.send(&req).await?; 51 | Ok(self 52 | .underlying 53 | .recv::() 54 | .await? 55 | .into_result()) 56 | } 57 | 58 | pub fn into_inner(self) -> rpc::Connection { 59 | self.underlying 60 | } 61 | } 62 | impl Deref for Connection { 63 | type Target = rpc::Connection; 64 | 65 | fn deref(&self) -> &Self::Target { 66 | &self.underlying 67 | } 68 | } 69 | impl DerefMut for Connection { 70 | fn deref_mut(&mut self) -> &mut Self::Target { 71 | &mut self.underlying 72 | } 73 | } 74 | 75 | impl crate::Connection for Connection { 76 | type Invoke<'a, T: 'a> = 77 | Pin, IpcError>> + Send + 'a>>; 78 | 79 | fn invoke<'a, P: Serialize + Send + 'a, T: DeserializeOwned + 'a>( 80 | &'a mut self, 81 | method: &'a str, 82 | params: P, 83 | ) -> Self::Invoke<'a, T> { 84 | Box::pin(self.invoke(method, params)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /airup-sdk/src/nonblocking/rpc.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::ApiError, 3 | rpc::{Error as IpcError, MessageProto, Request, Response}, 4 | }; 5 | use serde::{Serialize, de::DeserializeOwned}; 6 | use std::{ 7 | future::Future, 8 | io::Cursor, 9 | ops::{Deref, DerefMut}, 10 | path::Path, 11 | }; 12 | use tokio::{ 13 | io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, 14 | net::{UnixListener, UnixStream}, 15 | }; 16 | 17 | #[derive(Debug)] 18 | pub struct Connection(MessageProto); 19 | impl Connection { 20 | /// Connects to the specified socket. 21 | pub async fn connect>(path: P) -> std::io::Result { 22 | Ok(Self(MessageProto::new( 23 | UnixStream::connect(path).await?, 24 | usize::MAX, 25 | ))) 26 | } 27 | 28 | /// Receives a datagram and deserializes it from CBOR to `T`. 29 | pub async fn recv(&mut self) -> Result { 30 | let mut buf = Vec::new(); 31 | self.0.recv(&mut buf).await?; 32 | Ok(ciborium::from_reader(&buf[..])?) 33 | } 34 | 35 | /// Receives a request from the underlying protocol. 36 | pub async fn recv_req(&mut self) -> Result { 37 | let mut buf = Vec::new(); 38 | self.0.recv(&mut buf).await?; 39 | let req: Request = ciborium::from_reader(&buf[..]).unwrap_or_else(|err| { 40 | Request::new( 41 | "debug.echo_raw", 42 | Response::Err(ApiError::bad_request("InvalidCbor", err.to_string())), 43 | ) 44 | }); 45 | Ok(req) 46 | } 47 | 48 | /// Sends a datagram with CBOR-serialized given object. 49 | pub async fn send(&mut self, obj: &T) -> Result<(), IpcError> { 50 | let mut buffer = Cursor::new(Vec::with_capacity(128)); 51 | ciborium::into_writer(obj, &mut buffer)?; 52 | self.0.send(&buffer.into_inner()).await 53 | } 54 | 55 | /// Returns the underlying message protocol. 56 | pub fn into_inner(self) -> MessageProto { 57 | self.0 58 | } 59 | } 60 | impl Deref for Connection { 61 | type Target = MessageProto; 62 | 63 | fn deref(&self) -> &Self::Target { 64 | &self.0 65 | } 66 | } 67 | impl DerefMut for Connection { 68 | fn deref_mut(&mut self) -> &mut Self::Target { 69 | &mut self.0 70 | } 71 | } 72 | 73 | /// A wrap of `UnixListener` that accepts [`Connection`]. 74 | #[derive(Debug)] 75 | pub struct Server(UnixListener); 76 | impl Server { 77 | /// Creates a new instance, binding to the given path. 78 | pub fn new>(path: P) -> std::io::Result { 79 | Ok(Self(UnixListener::bind(path)?)) 80 | } 81 | 82 | /// Accepts an connection. 83 | pub async fn accept(&self) -> std::io::Result { 84 | Ok(Connection(self.0.accept().await?.0.into())) 85 | } 86 | } 87 | 88 | pub trait MessageProtoRecvExt { 89 | /// Receives a message from the stream. 90 | fn recv(&mut self, buf: &mut Vec) -> impl Future>; 91 | } 92 | pub trait MessageProtoSendExt { 93 | /// Sends a message to the stream. 94 | fn send(&mut self, blob: &[u8]) -> impl Future>; 95 | } 96 | impl MessageProtoRecvExt for MessageProto { 97 | async fn recv(&mut self, buf: &mut Vec) -> Result<(), IpcError> { 98 | let len = self.inner.read_u64_le().await? as usize; 99 | if len > self.size_limit { 100 | return Err(IpcError::MessageTooLong(len)); 101 | } 102 | buf.resize(len, 0u8); 103 | self.inner.read_exact(buf).await?; 104 | 105 | Ok(()) 106 | } 107 | } 108 | impl MessageProtoSendExt for MessageProto { 109 | async fn send(&mut self, blob: &[u8]) -> Result<(), IpcError> { 110 | self.inner.write_u64_le(blob.len() as _).await?; 111 | self.inner.write_all(blob).await?; 112 | 113 | Ok(()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /airup-sdk/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! The Airup SDK preludes. 2 | 3 | pub use crate::debug::ConnectionExt as _; 4 | pub use crate::error::IntoApiError; 5 | pub use crate::info::ConnectionExt as _; 6 | pub use crate::system::{ConnectionExt as _, QueryService, QuerySystem, Status}; 7 | 8 | cfg_if::cfg_if! { 9 | if #[cfg(feature = "nonblocking")] { 10 | pub use crate::nonblocking::Connection; 11 | pub use crate::nonblocking::fs::DirChain; 12 | pub use crate::nonblocking::files::*; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /airup-sdk/src/util.rs: -------------------------------------------------------------------------------- 1 | /// An extension of [`Iterator`]. 2 | pub trait IterExt { 3 | /// Removes *all* duplicated elements from the iterator. 4 | fn dedup_all(&mut self) -> Vec; 5 | } 6 | impl IterExt for I 7 | where 8 | I: Iterator, 9 | T: PartialEq, 10 | { 11 | fn dedup_all(&mut self) -> Vec { 12 | let mut result = Vec::new(); 13 | self.for_each(|x| { 14 | if !result.contains(&x) { 15 | result.push(x); 16 | } 17 | }); 18 | result 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /airup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airup" 3 | authors = ["sisungo "] 4 | version = "0.10.8" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | airup-sdk = { path = "../airup-sdk", features = ["_internal"] } 13 | anyhow = "1" 14 | ciborium = "0.2" 15 | clap = { version = "4", features = ["derive"] } 16 | chrono = "0.4" 17 | console = "0.15" 18 | mktemp = "0.5" 19 | serde_json = "1" 20 | -------------------------------------------------------------------------------- /airup/src/daemon.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | /// Start a new Airup daemon 4 | #[derive(Debug, Clone, Parser)] 5 | #[command(about)] 6 | pub struct Cmdline { 7 | #[arg(last = true)] 8 | args: Vec, 9 | } 10 | 11 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 12 | let temp = mktemp::Temp::new_file()?; 13 | let mut file = std::fs::File::options() 14 | .create(true) 15 | .truncate(true) 16 | .write(true) 17 | .open(&temp)?; 18 | ciborium::into_writer(airup_sdk::build::manifest(), &mut file)?; 19 | 20 | std::process::Command::new("airupd") 21 | .args(&cmdline.args) 22 | .arg("--build-manifest") 23 | .arg(temp.as_os_str()) 24 | .spawn()? 25 | .wait()?; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /airup/src/debug.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::{debug::ConnectionExt, prelude::*}; 2 | use anyhow::anyhow; 3 | use clap::{Parser, ValueEnum}; 4 | 5 | /// Debug options of Airup 6 | #[derive(Debug, Clone, Parser)] 7 | #[command(about)] 8 | pub struct Cmdline { 9 | /// Print build manifest acquired from Airup daemon 10 | #[arg(long)] 11 | print: Option, 12 | 13 | /// Reduce RPC if possible 14 | #[arg(long)] 15 | reduce_rpc: bool, 16 | 17 | /// Unregister an Airup extension 18 | #[arg(long)] 19 | unregister_extension: Option, 20 | 21 | /// Dump Airup's internal debug information 22 | #[arg(long)] 23 | dump: bool, 24 | 25 | /// Sets the server's instance name 26 | #[arg(long)] 27 | set_instance_name: Option, 28 | } 29 | 30 | #[derive(Debug, Clone, Copy, ValueEnum)] 31 | pub enum Printable { 32 | BuildManifest, 33 | } 34 | impl Printable { 35 | fn print(self, reduce_rpc: bool) -> anyhow::Result<()> { 36 | match self { 37 | Self::BuildManifest => print_build_manifest(reduce_rpc), 38 | } 39 | } 40 | } 41 | 42 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 43 | if let Some(printable) = cmdline.print { 44 | printable.print(cmdline.reduce_rpc) 45 | } else if let Some(name) = cmdline.unregister_extension { 46 | unregister_extension(&name) 47 | } else if cmdline.dump { 48 | dump() 49 | } else if let Some(name) = cmdline.set_instance_name { 50 | set_instance_name(&name) 51 | } else { 52 | Err(anyhow!("no action specified")) 53 | } 54 | } 55 | 56 | pub fn dump() -> anyhow::Result<()> { 57 | let mut conn = super::connect()?; 58 | let reply = conn.dump()??; 59 | 60 | println!("{reply}"); 61 | 62 | Ok(()) 63 | } 64 | 65 | pub fn set_instance_name(name: &str) -> anyhow::Result<()> { 66 | let mut conn = super::connect()?; 67 | conn.set_instance_name(name)??; 68 | Ok(()) 69 | } 70 | 71 | pub fn unregister_extension(name: &str) -> anyhow::Result<()> { 72 | let mut conn = super::connect()?; 73 | conn.unregister_extension(name)??; 74 | 75 | Ok(()) 76 | } 77 | 78 | pub fn print_build_manifest(reduce_rpc: bool) -> anyhow::Result<()> { 79 | let build_manifest = if reduce_rpc { 80 | serde_json::to_string_pretty(airup_sdk::build::manifest()) 81 | .expect("failed to serialize `airup_sdk::build::manifest()` into JSON") 82 | } else { 83 | serde_json::to_string_pretty(&super::connect()?.build_manifest()??) 84 | .expect("failed to serialize server side `BuildManifest` into JSON") 85 | }; 86 | println!("{}", build_manifest); 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /airup/src/disable.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::{ 2 | blocking::{files::*, fs::DirChain}, 3 | files::{Milestone, milestone}, 4 | system::ConnectionExt as _, 5 | }; 6 | use anyhow::anyhow; 7 | use clap::Parser; 8 | use console::style; 9 | 10 | /// Disable a service 11 | #[derive(Debug, Clone, Parser)] 12 | #[command(about)] 13 | pub struct Cmdline { 14 | service: String, 15 | 16 | #[arg(short, long)] 17 | milestone: Option, 18 | } 19 | 20 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 21 | let service = cmdline 22 | .service 23 | .strip_suffix(".airs") 24 | .unwrap_or(&cmdline.service); 25 | 26 | let mut conn = super::connect()?; 27 | 28 | let query_system = conn 29 | .query_system()? 30 | .map_err(|x| anyhow!("failed to query system information: {x}"))?; 31 | let current_milestone = query_system 32 | .milestones 33 | .last() 34 | .map(|x| &x.name[..]) 35 | .unwrap_or_else(|| { 36 | eprintln!( 37 | "{} failed to get current milestone, writing to `default` milestone!", 38 | style("warning:").yellow().bold() 39 | ); 40 | "default" 41 | }); 42 | let milestones = DirChain::new(&airup_sdk::build::manifest().milestone_dir); 43 | let milestone = cmdline 44 | .milestone 45 | .unwrap_or_else(|| current_milestone.into()); 46 | let milestone = milestones 47 | .find(format!("{milestone}.airm")) 48 | .ok_or_else(|| anyhow!("failed to get milestone `{milestone}`: milestone not found"))?; 49 | let milestone = 50 | Milestone::read_from(milestone).map_err(|x| anyhow!("failed to read milestone: {x}"))?; 51 | let chain = DirChain::new(&milestone.base_dir); 52 | 53 | let path = chain 54 | .find_or_create("97-auto-generated.list.airf") 55 | .map_err(|x| anyhow!("failed to open list file: {x}"))?; 56 | 57 | let old = std::fs::read_to_string(&path).unwrap_or_default(); 58 | let mut new = String::with_capacity(old.len()); 59 | let mut disabled = false; 60 | 61 | for x in old.lines() { 62 | if let Ok(item) = x.parse::() { 63 | match item { 64 | milestone::Item::Start(x) if x.strip_suffix(".airs").unwrap_or(&x) == service => { 65 | disabled = true; 66 | } 67 | milestone::Item::Cache(x) if x.strip_suffix(".airs").unwrap_or(&x) == service => { 68 | disabled = true; 69 | } 70 | _ => { 71 | new.push_str(x); 72 | new.push('\n'); 73 | } 74 | }; 75 | } 76 | } 77 | 78 | std::fs::write(&path, new.as_bytes())?; 79 | 80 | if !disabled { 81 | eprintln!( 82 | "{} service {} have not been enabled yet!", 83 | style("warning:").yellow().bold(), 84 | service 85 | ); 86 | } 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /airup/src/edit.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::{ 2 | blocking::{files, fs::DirChain}, 3 | files::Service, 4 | }; 5 | use anyhow::anyhow; 6 | use clap::Parser; 7 | use std::path::{Path, PathBuf}; 8 | 9 | /// Edit Airup files 10 | #[derive(Debug, Clone, Parser)] 11 | #[command(about)] 12 | pub struct Cmdline { 13 | file: String, 14 | } 15 | 16 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 17 | let editor = get_editor()?; 18 | 19 | if cmdline.file.strip_suffix(".airs").is_some() { 20 | do_edit(&editor, &find_or_create_service(&cmdline.file)?, |s| { 21 | files::read_merge::(vec![s.into()])?; 22 | Ok(()) 23 | })?; 24 | } else if let Some(x) = cmdline.file.strip_suffix(".airc") { 25 | let service = find_or_create_service(&format!("{x}.airs"))?; 26 | do_edit(&editor, &find_or_create_config(&cmdline.file)?, |s| { 27 | files::read_merge::(vec![service, s.into()])?; 28 | Ok(()) 29 | })?; 30 | } else { 31 | let (n, name) = cmdline.file.split('.').enumerate().last().unwrap(); 32 | if n > 0 { 33 | return Err(anyhow!("unknown file suffix `{name}`")); 34 | } else { 35 | return Err(anyhow!("file suffix must be specified to edit")); 36 | } 37 | } 38 | 39 | println!("note: You may run `airup self-reload` to ensure necessary cache to be refreshed."); 40 | 41 | Ok(()) 42 | } 43 | 44 | fn get_editor() -> anyhow::Result { 45 | if let Ok(x) = std::env::var("EDITOR") { 46 | Ok(x) 47 | } else if let Ok(x) = std::env::var("VISUAL") { 48 | Ok(x) 49 | } else { 50 | Err(anyhow!( 51 | "cannot find an editor: neither `$EDITOR` nor `$VISUAL` is set" 52 | )) 53 | } 54 | } 55 | 56 | fn do_edit( 57 | editor: &str, 58 | path: &Path, 59 | check: impl FnOnce(&Path) -> anyhow::Result<()>, 60 | ) -> anyhow::Result<()> { 61 | let temp = mktemp::Temp::new_file()?; 62 | std::fs::copy(path, &temp)?; 63 | std::process::Command::new(editor) 64 | .arg(temp.as_os_str()) 65 | .status()?; 66 | check(&temp)?; 67 | std::fs::copy(&temp, path)?; 68 | Ok(()) 69 | } 70 | 71 | fn find_or_create_service(name: &str) -> std::io::Result { 72 | DirChain::new(&airup_sdk::build::manifest().service_dir).find_or_create(name) 73 | } 74 | 75 | fn find_or_create_config(name: &str) -> std::io::Result { 76 | DirChain::new(&airup_sdk::build::manifest().config_dir).find_or_create(name) 77 | } 78 | -------------------------------------------------------------------------------- /airup/src/enable.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::{ 2 | blocking::{files::*, fs::DirChain}, 3 | files::{Milestone, milestone}, 4 | system::ConnectionExt as _, 5 | }; 6 | use anyhow::anyhow; 7 | use clap::Parser; 8 | use console::style; 9 | use std::io::Write; 10 | 11 | /// Enable a service 12 | #[derive(Debug, Clone, Parser)] 13 | #[command(about)] 14 | pub struct Cmdline { 15 | service: String, 16 | 17 | #[arg(short, long)] 18 | force: bool, 19 | 20 | #[arg(short, long)] 21 | cache: bool, 22 | 23 | #[arg(short, long)] 24 | milestone: Option, 25 | } 26 | 27 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 28 | let service = cmdline 29 | .service 30 | .strip_suffix(".airs") 31 | .unwrap_or(&cmdline.service); 32 | 33 | let mut conn = super::connect()?; 34 | 35 | let query_system = conn 36 | .query_system()? 37 | .map_err(|x| anyhow!("failed to query system information: {x}"))?; 38 | let current_milestone = query_system 39 | .milestones 40 | .last() 41 | .map(|x| &x.name[..]) 42 | .unwrap_or_else(|| { 43 | eprintln!( 44 | "{} failed to get current milestone, writing to `default` milestone!", 45 | style("warning:").yellow().bold() 46 | ); 47 | "default" 48 | }); 49 | let milestones = DirChain::new(&airup_sdk::build::manifest().milestone_dir); 50 | let milestone = cmdline 51 | .milestone 52 | .unwrap_or_else(|| current_milestone.into()); 53 | let milestone = milestones 54 | .find(format!("{milestone}.airm")) 55 | .ok_or_else(|| anyhow!("failed to get milestone `{milestone}`: milestone not found"))?; 56 | let milestone = 57 | Milestone::read_from(milestone).map_err(|x| anyhow!("failed to read milestone: {x}"))?; 58 | let chain = DirChain::new(&milestone.base_dir); 59 | 60 | for item in milestone.items() { 61 | match item { 62 | milestone::Item::Start(x) if x.strip_suffix(".airs").unwrap_or(&x) == service => { 63 | eprintln!( 64 | "{} service {} have already been enabled", 65 | style("warning:").yellow().bold(), 66 | service 67 | ); 68 | std::process::exit(0); 69 | } 70 | milestone::Item::Cache(x) 71 | if x.strip_suffix(".airs").unwrap_or(&x) == service && cmdline.cache => 72 | { 73 | eprintln!( 74 | "{} service {} have already been enabled", 75 | style("warning:").yellow().bold(), 76 | service 77 | ); 78 | std::process::exit(0); 79 | } 80 | _ => (), 81 | } 82 | } 83 | 84 | if !cmdline.force { 85 | conn.query_service(service)? 86 | .map_err(|x| anyhow!("failed to enable service `{}`: {}", service, x))?; 87 | } 88 | 89 | let file = chain 90 | .find_or_create("97-auto-generated.list.airf") 91 | .map_err(|x| anyhow!("failed to open list file: {x}"))?; 92 | let mut file = std::fs::File::options() 93 | .create(true) 94 | .append(true) 95 | .open(file) 96 | .map_err(|x| anyhow!("failed to open list file: {x}"))?; 97 | 98 | if cmdline.cache { 99 | file.write_all(format!("\ncache {}\n", service).as_bytes()) 100 | .map_err(|x| anyhow!("failed to write to list file: {x}"))?; 101 | } else { 102 | file.write_all(format!("\nstart {}\n", service).as_bytes()) 103 | .map_err(|x| anyhow!("failed to write to list file: {x}"))?; 104 | } 105 | 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /airup/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Command-line utility for accessing Airup facilities. 2 | 3 | mod daemon; 4 | mod debug; 5 | mod disable; 6 | mod edit; 7 | mod enable; 8 | mod query; 9 | mod reboot; 10 | mod reload; 11 | mod restart; 12 | mod self_reload; 13 | mod start; 14 | mod stop; 15 | mod trigger_event; 16 | mod util; 17 | 18 | use anyhow::anyhow; 19 | use clap::Parser; 20 | use console::style; 21 | use std::path::{Path, PathBuf}; 22 | 23 | #[derive(Parser)] 24 | #[command(author, version, about, long_about = None)] 25 | enum Subcommand { 26 | Start(start::Cmdline), 27 | Stop(stop::Cmdline), 28 | Reload(reload::Cmdline), 29 | Restart(restart::Cmdline), 30 | Query(query::Cmdline), 31 | SelfReload(self_reload::Cmdline), 32 | Reboot(reboot::Cmdline), 33 | Edit(edit::Cmdline), 34 | Enable(enable::Cmdline), 35 | Disable(disable::Cmdline), 36 | TriggerEvent(trigger_event::Cmdline), 37 | Daemon(daemon::Cmdline), 38 | Debug(debug::Cmdline), 39 | } 40 | impl Subcommand { 41 | fn execute(self) -> anyhow::Result<()> { 42 | match self { 43 | Self::Start(cmdline) => start::main(cmdline), 44 | Self::Stop(cmdline) => stop::main(cmdline), 45 | Self::Reload(cmdline) => reload::main(cmdline), 46 | Self::Restart(cmdline) => restart::main(cmdline), 47 | Self::Query(cmdline) => query::main(cmdline), 48 | Self::Reboot(cmdline) => reboot::main(cmdline), 49 | Self::SelfReload(cmdline) => self_reload::main(cmdline), 50 | Self::Edit(cmdline) => edit::main(cmdline), 51 | Self::Enable(cmdline) => enable::main(cmdline), 52 | Self::Disable(cmdline) => disable::main(cmdline), 53 | Self::TriggerEvent(cmdline) => trigger_event::main(cmdline), 54 | Self::Daemon(cmdline) => daemon::main(cmdline), 55 | Self::Debug(cmdline) => debug::main(cmdline), 56 | } 57 | } 58 | } 59 | 60 | #[derive(Parser)] 61 | struct Cmdline { 62 | #[command(subcommand)] 63 | subcommand: Subcommand, 64 | 65 | /// Override default build manifest 66 | #[arg(long)] 67 | build_manifest: Option, 68 | } 69 | impl Cmdline { 70 | fn execute(self) -> anyhow::Result<()> { 71 | set_build_manifest_at(self.build_manifest.as_deref())?; 72 | self.subcommand.execute()?; 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | fn main() { 79 | let cmdline = Cmdline::parse(); 80 | 81 | if let Err(e) = cmdline.execute() { 82 | eprintln!("{} {}", style("error:").red().bold(), e); 83 | std::process::exit(1); 84 | } 85 | } 86 | 87 | pub fn connect() -> anyhow::Result { 88 | airup_sdk::blocking::Connection::connect(airup_sdk::socket_path()) 89 | .map_err(|e| anyhow!("cannot connect to airup daemon: {}", e)) 90 | } 91 | 92 | fn set_build_manifest_at(path: Option<&Path>) -> anyhow::Result<()> { 93 | if let Some(path) = path { 94 | airup_sdk::build::set_manifest( 95 | serde_json::from_slice( 96 | &std::fs::read(path) 97 | .map_err(|err| anyhow!("failed to read overridden build manifest: {err}"))?, 98 | ) 99 | .map_err(|err| anyhow!("failed to parse overridden build manifest: {err}"))?, 100 | ); 101 | } 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /airup/src/reboot.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::system::ConnectionExt as _; 2 | use clap::Parser; 3 | 4 | /// Reboot, power-off or halt the system 5 | #[derive(Debug, Clone, Parser)] 6 | #[command(about)] 7 | pub struct Cmdline { 8 | /// Specify the reboot mode 9 | #[arg(default_value = "reboot")] 10 | mode: String, 11 | } 12 | 13 | pub fn main(mut cmdline: Cmdline) -> anyhow::Result<()> { 14 | let mut conn = super::connect()?; 15 | 16 | if cmdline.mode == "userspace" { 17 | cmdline.mode = "userspace-reboot".into(); 18 | } 19 | 20 | _ = conn.enter_milestone(&cmdline.mode)?; 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /airup/src/reload.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::system::ConnectionExt as _; 2 | use anyhow::anyhow; 3 | use clap::Parser; 4 | 5 | /// Notify a service to reload its status 6 | #[derive(Debug, Clone, Parser)] 7 | #[command(about)] 8 | pub struct Cmdline { 9 | service: String, 10 | } 11 | 12 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 13 | let mut conn = super::connect()?; 14 | 15 | if let "airupd" | "airupd.airs" = &cmdline.service[..] { 16 | if let Some("& airup self-reload") = conn 17 | .query_service(&cmdline.service)?? 18 | .definition 19 | .exec 20 | .reload 21 | .as_deref() 22 | { 23 | conn.refresh()??; 24 | return Ok(()); 25 | } 26 | } 27 | 28 | conn.reload_service(&cmdline.service)? 29 | .map_err(|e| anyhow!("failed to reload service `{}`: {}", cmdline.service, e))?; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /airup/src/restart.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::system::ConnectionExt as _; 2 | use anyhow::anyhow; 3 | use clap::Parser; 4 | 5 | /// Restart a service 6 | #[derive(Debug, Clone, Parser)] 7 | #[command(about)] 8 | pub struct Cmdline { 9 | service: String, 10 | 11 | /// Restart the service if already started, otherwise start it 12 | #[arg(short = 'E', long)] 13 | effective: bool, 14 | } 15 | 16 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 17 | let mut conn = super::connect()?; 18 | 19 | let stop = conn.stop_service(&cmdline.service)?; 20 | if !cmdline.effective { 21 | stop.map_err(|e| anyhow!("failed to stop service `{}`: {}", cmdline.service, e))?; 22 | } 23 | 24 | conn.start_service(&cmdline.service)? 25 | .map_err(|e| anyhow!("failed to start service `{}`: {}", cmdline.service, e))?; 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /airup/src/self_reload.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::system::ConnectionExt as _; 2 | use clap::Parser; 3 | 4 | /// Reload the `airupd` daemon itself 5 | #[derive(Debug, Clone, Parser)] 6 | #[command(about)] 7 | pub struct Cmdline { 8 | /// Notify the daemon to decrease memory usage 9 | #[arg(long)] 10 | gc: bool, 11 | } 12 | 13 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 14 | let mut conn = super::connect()?; 15 | conn.refresh()??; 16 | if cmdline.gc { 17 | conn.gc()??; 18 | } 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /airup/src/start.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::{blocking::files, system::ConnectionExt as _}; 2 | use anyhow::anyhow; 3 | use clap::Parser; 4 | use std::path::PathBuf; 5 | 6 | /// Start a service 7 | #[derive(Debug, Clone, Parser)] 8 | #[command(about)] 9 | pub struct Cmdline { 10 | service: String, 11 | 12 | /// Cache the service only 13 | #[arg(long)] 14 | cache: bool, 15 | 16 | /// Sideload a service 17 | #[arg(long)] 18 | sideload: Option, 19 | } 20 | 21 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 22 | let mut conn = super::connect()?; 23 | 24 | if let Some(path) = &cmdline.sideload { 25 | let service = files::read_merge(vec![path.clone()]) 26 | .map_err(|e| anyhow!("failed to read service at `{}`: {}", path.display(), e))?; 27 | conn.sideload_service(&cmdline.service, &service)??; 28 | } 29 | 30 | if !cmdline.cache { 31 | conn.start_service(&cmdline.service)? 32 | .map_err(|e| anyhow!("failed to start service `{}`: {}", cmdline.service, e))?; 33 | } else if cmdline.sideload.is_none() { 34 | conn.cache_service(&cmdline.service)? 35 | .map_err(|e| anyhow!("failed to cache service `{}`: {}", cmdline.service, e))?; 36 | } 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /airup/src/stop.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::system::ConnectionExt as _; 2 | use anyhow::anyhow; 3 | use clap::Parser; 4 | 5 | /// Stop a service 6 | #[derive(Debug, Clone, Parser)] 7 | #[command(about)] 8 | pub struct Cmdline { 9 | service: String, 10 | 11 | /// Uncache the service 12 | #[arg(long)] 13 | uncache: bool, 14 | 15 | /// Force the service to stop 16 | #[arg(short, long)] 17 | force: bool, 18 | } 19 | 20 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 21 | let mut conn = super::connect()?; 22 | 23 | let mut stop_service = || { 24 | if cmdline.force { 25 | conn.kill_service(&cmdline.service) 26 | } else { 27 | conn.stop_service(&cmdline.service) 28 | } 29 | }; 30 | 31 | if !cmdline.uncache { 32 | stop_service()? 33 | .map_err(|e| anyhow!("failed to stop service `{}`: {}", cmdline.service, e))?; 34 | } else { 35 | stop_service()?.ok(); 36 | } 37 | 38 | if cmdline.uncache { 39 | conn.uncache_service(&cmdline.service)? 40 | .map_err(|e| anyhow!("failed to uncache service `{}`: {}", cmdline.service, e))?; 41 | } 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /airup/src/trigger_event.rs: -------------------------------------------------------------------------------- 1 | use airup_sdk::system::{ConnectionExt as _, Event}; 2 | use clap::Parser; 3 | 4 | /// Trigger an event in the event bus 5 | #[derive(Debug, Clone, Parser)] 6 | #[command(about)] 7 | pub struct Cmdline { 8 | id: String, 9 | 10 | #[arg(short, long, default_value_t)] 11 | payload: String, 12 | } 13 | 14 | pub fn main(cmdline: Cmdline) -> anyhow::Result<()> { 15 | let mut conn = super::connect()?; 16 | 17 | let event = Event::new(cmdline.id, cmdline.payload); 18 | conn.trigger_event(&event)??; 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /airup/src/util.rs: -------------------------------------------------------------------------------- 1 | /// Formats size byte number into human-readable string. 2 | pub fn format_size(bytes: u64) -> String { 3 | match bytes { 4 | 0..=999 => format!("{} B", bytes), 5 | 1000..=999_999 => format!("{} KB", ((bytes as f64) / 1000.).round()), 6 | 1_000_000..=9_999_999_999 => format!("{:.2} MB", ((bytes as f64) / 1_000_000.)), 7 | 10_000_000_000.. => format!("{:.2} GB", ((bytes as f64) / 1_000_000_000.)), 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /airupd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupd" 3 | authors = ["sisungo "] 4 | version = "0.10.8" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | default = ["cgroups"] 13 | cgroups = ["airupfx/cgroups"] 14 | selinux = ["airupfx/selinux"] 15 | 16 | [dependencies] 17 | anyhow = "1" 18 | async-broadcast = "0.7" 19 | airupfx = { path = "../airupfx/airupfx" } 20 | airup-sdk = { path = "../airup-sdk", features = ["_internal"] } 21 | ciborium = "0.2" 22 | libc = "0.2" 23 | peg = "0.8" 24 | thiserror = "1" 25 | tokio = { workspace = true } 26 | tracing = "0.1" 27 | tracing-subscriber = "0.3" 28 | -------------------------------------------------------------------------------- /airupd/src/ace/builtins.rs: -------------------------------------------------------------------------------- 1 | //! Built-in commands of ACE. 2 | 3 | use airupfx::process::ExitStatus; 4 | use libc::SIGTERM; 5 | use std::{collections::HashMap, hash::BuildHasher, time::Duration}; 6 | use tokio::task::JoinHandle; 7 | 8 | pub type BuiltinModule = fn(args: Vec) -> JoinHandle; 9 | 10 | pub fn init(builtins: &mut HashMap<&'static str, BuiltinModule, H>) { 11 | builtins.insert("noop", noop); 12 | builtins.insert("console.setup", console_setup); 13 | builtins.insert("console.info", console_info); 14 | builtins.insert("console.warn", console_warn); 15 | builtins.insert("console.error", console_error); 16 | builtins.insert("builtin.sleep", sleep); 17 | } 18 | 19 | pub fn console_setup(args: Vec) -> JoinHandle { 20 | tokio::spawn(async move { 21 | let path = match args.first() { 22 | Some(x) => x, 23 | None => return 1, 24 | }; 25 | match airupfx::env::setup_stdio(path.as_ref()).await { 26 | Ok(()) => 0, 27 | Err(_) => 2, 28 | } 29 | }) 30 | } 31 | 32 | pub fn console_info(args: Vec) -> JoinHandle { 33 | tracing::info!(target: "console", "{}", merge_args(&args)); 34 | tokio::spawn(async { 0 }) 35 | } 36 | 37 | pub fn console_warn(args: Vec) -> JoinHandle { 38 | tracing::warn!(target: "console", "{}", merge_args(&args)); 39 | tokio::spawn(async { 0 }) 40 | } 41 | 42 | pub fn console_error(args: Vec) -> JoinHandle { 43 | tracing::error!(target: "console", "{}", merge_args(&args)); 44 | tokio::spawn(async { 0 }) 45 | } 46 | 47 | pub fn noop(_: Vec) -> JoinHandle { 48 | tokio::spawn(async { 0 }) 49 | } 50 | 51 | pub fn sleep(args: Vec) -> JoinHandle { 52 | tokio::spawn(async move { 53 | let duration = match args.first() { 54 | Some(x) => x, 55 | None => return 1, 56 | }; 57 | let duration: u64 = match duration.parse() { 58 | Ok(x) => x, 59 | Err(_) => return 2, 60 | }; 61 | 62 | tokio::time::sleep(Duration::from_millis(duration)).await; 63 | 64 | 0 65 | }) 66 | } 67 | 68 | pub async fn wait(rx: &mut JoinHandle) -> ExitStatus { 69 | (rx.await).map_or(ExitStatus::Signaled(SIGTERM), |code| { 70 | ExitStatus::Exited(code as _) 71 | }) 72 | } 73 | 74 | fn merge_args(args: &[String]) -> String { 75 | let mut result = String::with_capacity(args.len() * 12); 76 | for arg in args { 77 | result.push_str(arg); 78 | result.push(' '); 79 | } 80 | result.pop(); 81 | result 82 | } 83 | -------------------------------------------------------------------------------- /airupd/src/ace/parser.rs: -------------------------------------------------------------------------------- 1 | //! A parser based on `rust-peg`. 2 | 3 | peg::parser! { 4 | grammar ace() for str { 5 | // ==- Commons -== 6 | rule _() 7 | = quiet!{comment()} 8 | / quiet!{[x if x.is_ascii_whitespace()]} 9 | 10 | // ==- Comments -== 11 | rule comment() 12 | = quiet! {"#" [^ '\n']* "\n"} 13 | / expected!("comment") 14 | 15 | // ==- Text Literals -== 16 | rule ascii_escape() -> char 17 | = "\\" esc:[x if ['n', 'r', 't', '\\', '0'].contains(&x)] { map_ascii_escape(esc) } 18 | 19 | rule quote_escape() -> char 20 | = "\\" esc:[x if ['\'', '"'].contains(&x)] { esc } 21 | 22 | rule text_literal_escape() -> char 23 | = x:(ascii_escape() / quote_escape()) { x } 24 | 25 | rule strong_string_literal() -> String 26 | = "\"" s:(text_literal_escape() / [^ '\n' | '\r' | '\\' | '\"'])* "\"" { s.into_iter().collect() } 27 | 28 | rule weak_string_literal() -> &'input str 29 | = s:$([^ '$' | '"'] [^ '\n' | '\r' | '\\' | '\"' | ' ']*) { s } 30 | 31 | rule string_literal() -> String 32 | = quiet! {x:strong_string_literal() { x }} 33 | / quiet! {x:weak_string_literal() { x.into() }} 34 | / expected!("string literal") 35 | 36 | // ==- Variables -== 37 | rule ident() -> &'input str 38 | = s:$([^ x if (x.is_ascii_punctuation() && x != '_' && x != '-') || x.is_whitespace() ]*) { s } 39 | 40 | rule variable() -> &'input str 41 | = "${" s:ident() "}" { s } 42 | 43 | // ==- Expressions -== 44 | rule expr() -> String 45 | = v:variable() {? std::env::var(v).or(Err("environment not found")) } 46 | / s:string_literal() { s } 47 | 48 | pub rule command() -> Command 49 | = _* module:expr() _+ args:(expr() ** (_*)) _* { 50 | Command { module, args } 51 | } 52 | } 53 | } 54 | 55 | fn map_ascii_escape(x: char) -> char { 56 | match x { 57 | 'n' => '\n', 58 | 'r' => '\r', 59 | 't' => '\t', 60 | '\\' => '\\', 61 | '0' => '\0', 62 | _ => unreachable!(), 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 67 | pub struct Command { 68 | pub module: String, 69 | pub args: Vec, 70 | } 71 | impl Command { 72 | /// Parses a command. 73 | pub fn parse(s: &str) -> Result { 74 | let s = format!("{s} "); 75 | Ok(ace::command(&s)?) 76 | } 77 | 78 | /// Wraps a `sudo`-pattern command. 79 | pub fn wrap(mut self, f: impl FnOnce(Self) -> T) -> Option { 80 | if self.args.is_empty() { 81 | return None; 82 | } 83 | let module = self.args.remove(0); 84 | Some(f(Self { 85 | module, 86 | args: self.args, 87 | })) 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | #[test] 93 | fn tests() { 94 | unsafe { 95 | std::env::set_var("TEST_ENV", "It works!"); 96 | } 97 | 98 | assert_eq!( 99 | Command::parse("echo \"Hello, world!\"").unwrap(), 100 | Command { 101 | module: "echo".into(), 102 | args: vec!["Hello, world!".into()], 103 | } 104 | ); 105 | assert_eq!( 106 | Command::parse("echo ${TEST_ENV} ").unwrap(), 107 | Command { 108 | module: "echo".into(), 109 | args: vec!["It works!".into()], 110 | } 111 | ); 112 | assert_eq!( 113 | Command::parse("echo -n Hello, world!").unwrap(), 114 | Command { 115 | module: "echo".into(), 116 | args: vec!["-n".into(), "Hello,".into(), "world!".into()], 117 | } 118 | ); 119 | assert_eq!( 120 | Command::parse("/bin/create").unwrap(), 121 | Command { 122 | module: "/bin/create".into(), 123 | args: vec![], 124 | } 125 | ); 126 | 127 | Command::parse("echo \"Hello, world!").unwrap_err(); 128 | Command::parse("echo ${__ENV_NON_EXISTENT__}").unwrap_err(); 129 | } 130 | -------------------------------------------------------------------------------- /airupd/src/env.rs: -------------------------------------------------------------------------------- 1 | //! Inspection and manipulation of `airupd`’s environment. 2 | 3 | use std::{borrow::Cow, path::PathBuf}; 4 | 5 | macro_rules! feed_parser { 6 | ($flag:ident, $storage:expr, $arg:expr) => { 7 | if $flag { 8 | $storage = $arg; 9 | $flag = false; 10 | continue; 11 | } 12 | }; 13 | } 14 | 15 | /// Represents to Airup's command-line arguments. 16 | #[derive(Debug, Clone)] 17 | pub struct Cmdline { 18 | /// Enable verbose console outputs 19 | pub verbose: bool, 20 | 21 | /// Disable console outputs 22 | pub quiet: bool, 23 | 24 | /// Disable colorful console outputs 25 | pub no_color: bool, 26 | 27 | /// Specify bootstrap milestone 28 | pub milestone: Cow<'static, str>, 29 | 30 | /// Overriding build manifest path 31 | pub build_manifest: Option, 32 | } 33 | impl Cmdline { 34 | /// Parses a new [`Cmdline`] instance from the command-line arguments. This function will automatically detect the 35 | /// environment to detect the style of the parser. 36 | pub fn parse() -> Self { 37 | if cfg!(target_os = "linux") 38 | && airupfx::process::as_pid1() 39 | && !matches!( 40 | airupfx::env::take_var("AIRUP_CMDLINE").as_deref(), 41 | Ok("unix") 42 | ) 43 | { 44 | Self::parse_as_linux_init() 45 | } else { 46 | Self::parse_as_unix_command() 47 | } 48 | } 49 | 50 | /// A command-line argument parser that assumes arguments are Linux-init styled. 51 | fn parse_as_linux_init() -> Self { 52 | let mut object = Self::default(); 53 | 54 | for arg in std::env::args() { 55 | if arg == "single" { 56 | object.milestone = "single-user".into(); 57 | } 58 | } 59 | 60 | if let Ok(x) = airupfx::env::take_var("AIRUP_MILESTONE") { 61 | object.milestone = x.into(); 62 | } 63 | 64 | if let Ok(options) = airupfx::env::take_var("AIRUP_CONOUT_POLICY") { 65 | for opt in options.split(',') { 66 | match opt { 67 | "quiet" => object.quiet = true, 68 | "nocolor" => object.no_color = true, 69 | "verbose" => object.verbose = true, 70 | _ => {} 71 | } 72 | } 73 | } 74 | 75 | object 76 | } 77 | 78 | /// A command-line argument parser that assumes arguments are passed like common Unix commands. 79 | fn parse_as_unix_command() -> Self { 80 | let mut object = Self::default(); 81 | 82 | let mut parsing_milestone = false; 83 | let mut parsing_build_manifest = false; 84 | for arg in std::env::args() { 85 | feed_parser!(parsing_milestone, object.milestone, arg.into()); 86 | feed_parser!( 87 | parsing_build_manifest, 88 | object.build_manifest, 89 | Some(arg.into()) 90 | ); 91 | 92 | match &arg[..] { 93 | "-m" | "--milestone" => parsing_milestone = true, 94 | "--build-manifest" => parsing_build_manifest = true, 95 | "--verbose" => object.verbose = true, 96 | "-q" | "--quiet" => object.quiet = true, 97 | "--no-color" => object.no_color = true, 98 | "-h" | "--help" => Self::print_help(), 99 | "-V" | "--version" => Self::print_version(), 100 | _ => (), 101 | } 102 | } 103 | 104 | object 105 | } 106 | 107 | /// Prints help information. 108 | fn print_help() -> ! { 109 | println!("Usage: airupd [OPTIONS]"); 110 | println!(); 111 | println!("Options:"); 112 | println!(" -h, --help Print help"); 113 | println!(" -V, --version Print version"); 114 | println!(" --build-manifest Override builtin build manifest"); 115 | println!(" -m, --milestone Specify bootstrap milestone"); 116 | println!(" -q, --quiet Disable console outputs"); 117 | println!(" --verbose Enable verbose console outputs"); 118 | println!(" --no-color Disable colorful console outputs"); 119 | std::process::exit(0); 120 | } 121 | 122 | /// Prints version information. 123 | fn print_version() -> ! { 124 | println!("airupd v{}", env!("CARGO_PKG_VERSION")); 125 | std::process::exit(0); 126 | } 127 | } 128 | impl Default for Cmdline { 129 | fn default() -> Self { 130 | Self { 131 | quiet: false, 132 | no_color: false, 133 | verbose: false, 134 | milestone: "default".into(), 135 | build_manifest: None, 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /airupd/src/events.rs: -------------------------------------------------------------------------------- 1 | //! Event subsystem of the Airup daemon. 2 | 3 | use airup_sdk::system::Event; 4 | 5 | /// The event bus. 6 | #[derive(Debug)] 7 | pub struct Bus { 8 | sender: async_broadcast::Sender, 9 | _receiver: async_broadcast::InactiveReceiver, 10 | } 11 | impl Bus { 12 | /// Creates a new [`Bus`] instance. 13 | pub fn new() -> Self { 14 | let (sender, _receiver) = async_broadcast::broadcast(16); 15 | let _receiver = _receiver.deactivate(); 16 | Self { sender, _receiver } 17 | } 18 | 19 | /// Subscribes to the bus. 20 | pub fn subscribe(&self) -> async_broadcast::Receiver { 21 | self.sender.new_receiver() 22 | } 23 | 24 | /// Triggers an event in the bus. 25 | pub async fn trigger(&self, event: Event) { 26 | self.sender 27 | .broadcast(event) 28 | .await 29 | .expect("the bus should be never closed"); 30 | } 31 | } 32 | impl Default for Bus { 33 | fn default() -> Self { 34 | Self::new() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /airupd/src/lifetime.rs: -------------------------------------------------------------------------------- 1 | //! Inspection and manipulation of the system's lifetime. 2 | 3 | use airupfx::prelude::*; 4 | use tokio::sync::broadcast; 5 | 6 | /// Airupd's lifetime manager. 7 | #[derive(Debug)] 8 | pub struct System(broadcast::Sender); 9 | impl System { 10 | /// Creates a new instance with default settings. 11 | pub fn new() -> Self { 12 | Self(broadcast::channel(1).0) 13 | } 14 | 15 | /// Creates a new [`broadcast::Receiver`] handle that will receive events sent after this call to `subscribe`. 16 | pub fn subscribe(&self) -> broadcast::Receiver { 17 | self.0.subscribe() 18 | } 19 | 20 | /// Makes `airupd` exit. 21 | pub fn exit(&self, code: i32) { 22 | self.send(Event::Exit(code)); 23 | } 24 | 25 | /// Powers the device off. 26 | pub fn poweroff(&self) { 27 | self.send(Event::PowerOff); 28 | } 29 | 30 | /// Reboots the device. 31 | pub fn reboot(&self) { 32 | self.send(Event::Reboot); 33 | } 34 | 35 | /// Halts the device. 36 | pub fn halt(&self) { 37 | self.send(Event::Halt); 38 | } 39 | 40 | /// Reboots the system's userspace. 41 | pub fn userspace_reboot(&self) { 42 | self.send(Event::UserspaceReboot); 43 | } 44 | 45 | /// Sends an process-wide lifetime event. 46 | fn send(&self, event: Event) { 47 | self.0.send(event).ok(); 48 | } 49 | } 50 | impl Default for System { 51 | fn default() -> Self { 52 | Self::new() 53 | } 54 | } 55 | 56 | /// An event related to Airupd's lifetime. 57 | #[derive(Debug, Clone)] 58 | pub enum Event { 59 | /// Makes `airupd` exit. 60 | Exit(i32), 61 | 62 | /// Powers the device off. 63 | PowerOff, 64 | 65 | /// Reboots the device. 66 | Reboot, 67 | 68 | /// Halts the device. 69 | Halt, 70 | 71 | /// Reboots the system's userspace. 72 | UserspaceReboot, 73 | } 74 | impl Event { 75 | /// Handles the event. 76 | pub async fn handle(&self) -> ! { 77 | _ = match self { 78 | Self::Exit(code) => std::process::exit(*code), 79 | Self::PowerOff => power_manager().poweroff().await, 80 | Self::Reboot => power_manager().reboot().await, 81 | Self::Halt => power_manager().halt().await, 82 | Self::UserspaceReboot => power_manager().userspace().await, 83 | }; 84 | 85 | std::process::exit(1); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /airupd/src/logging.rs: -------------------------------------------------------------------------------- 1 | //! Airupd-flavored presets for the [`tracing`] framework. 2 | 3 | use tracing::metadata::LevelFilter; 4 | use tracing_subscriber::{filter::filter_fn, prelude::*}; 5 | 6 | /// Builder of `airupd`-flavor tracing configuration. 7 | #[derive(Debug, Clone)] 8 | pub struct Builder { 9 | name: String, 10 | quiet: bool, 11 | verbose: bool, 12 | color: bool, 13 | } 14 | impl Builder { 15 | /// Creates a new [`Builder`] instance with default settings. 16 | #[inline] 17 | #[must_use] 18 | pub fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | /// Sets the logger's name. 23 | #[inline] 24 | pub fn name>(&mut self, s: S) -> &mut Self { 25 | self.name = s.into(); 26 | self 27 | } 28 | 29 | /// Sets whether console output is disabled for the logger. 30 | #[inline] 31 | pub fn quiet(&mut self, val: bool) -> &mut Self { 32 | self.quiet = val; 33 | self 34 | } 35 | 36 | /// Sets whether console output is verbose for the logger. 37 | #[inline] 38 | pub fn verbose(&mut self, val: bool) -> &mut Self { 39 | self.verbose = val; 40 | self 41 | } 42 | 43 | /// Sets whether colorful console output is enabled for the logger. 44 | #[inline] 45 | pub fn color(&mut self, val: bool) -> &mut Self { 46 | self.color = val; 47 | self 48 | } 49 | 50 | /// Initializes the logger. 51 | #[inline] 52 | pub fn install(&mut self) { 53 | let verbose = self.verbose; 54 | let level_filter = match self.quiet { 55 | true => LevelFilter::ERROR, 56 | false => match verbose { 57 | true => LevelFilter::TRACE, 58 | false => LevelFilter::INFO, 59 | }, 60 | }; 61 | 62 | let stdio_layer = tracing_subscriber::fmt::layer() 63 | .without_time() 64 | .with_ansi(self.color) 65 | .with_file(false) 66 | .with_writer(std::io::stderr) 67 | .with_target(false) 68 | .with_filter(filter_fn(move |metadata| { 69 | verbose || metadata.target().contains("console") 70 | })) 71 | .with_filter(level_filter); 72 | 73 | tracing_subscriber::registry().with(stdio_layer).init(); 74 | } 75 | } 76 | impl Default for Builder { 77 | fn default() -> Self { 78 | Self { 79 | name: "airupd".into(), 80 | quiet: false, 81 | verbose: false, 82 | color: true, 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /airupd/src/main.rs: -------------------------------------------------------------------------------- 1 | //! The Airup daemon. 2 | 3 | mod ace; 4 | mod app; 5 | mod env; 6 | mod events; 7 | mod extension; 8 | mod lifetime; 9 | mod logging; 10 | mod milestones; 11 | mod rpc; 12 | mod storage; 13 | mod supervisor; 14 | 15 | use airupfx::prelude::*; 16 | 17 | /// Entrypoint of the program. 18 | #[tokio::main(flavor = "current_thread")] 19 | async fn main() { 20 | airupfx::init().await; 21 | let cmdline = self::env::Cmdline::parse(); 22 | 23 | logging::Builder::new() 24 | .name("airupd") 25 | .quiet(cmdline.quiet) 26 | .color(!cmdline.no_color) 27 | .verbose(cmdline.verbose) 28 | .install(); 29 | app::set_manifest_at(cmdline.build_manifest.as_deref()).await; 30 | milestones::early_boot::enter().await; 31 | app::init().await; 32 | 33 | // Creates Airup runtime primitives 34 | app::airupd().storage.config.populate_system_config(); 35 | let _lock = app::airupd() 36 | .storage 37 | .runtime 38 | .lock() 39 | .await 40 | .unwrap_log("unable to lock database") 41 | .await; 42 | app::airupd() 43 | .start_rpc_server() 44 | .await 45 | .unwrap_log("failed to create airupd ipc socket") 46 | .await; 47 | app::airupd().set_signal_hooks(); 48 | 49 | if airupfx::process::as_pid1() && !cmdline.quiet { 50 | println!( 51 | "Welcome to {}!\n", 52 | app::airupd().storage.config.system_conf.system.os_name 53 | ); 54 | } 55 | 56 | app::airupd().bootstrap_milestone(cmdline.milestone.to_string()); 57 | 58 | let mut lifetime = app::airupd().lifetime.subscribe(); 59 | if let Ok(event) = lifetime.recv().await { 60 | drop(_lock); 61 | event.handle().await; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /airupd/src/milestones/early_boot.rs: -------------------------------------------------------------------------------- 1 | //! The `early_boot` pseudo-milestone. 2 | 3 | use crate::ace::Ace; 4 | use airupfx::prelude::*; 5 | 6 | /// Enters the `early_boot` pseudo-milestone. 7 | pub async fn enter() { 8 | let ace = Ace::default(); 9 | 10 | for i in &airup_sdk::build::manifest().early_cmds { 11 | if let Err(x) = super::run_wait(&ace, i).await { 12 | Err::<(), _>(x) 13 | .unwrap_log(&format!( 14 | "Failed to execute command `{i}` in `early_boot` milestone" 15 | )) 16 | .await; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /airupd/src/milestones/reboot.rs: -------------------------------------------------------------------------------- 1 | //! The `reboot` milestone preset series. 2 | 3 | use crate::app::airupd; 4 | use airup_sdk::Error; 5 | use std::{collections::HashSet, time::Duration}; 6 | use tokio::task::{JoinHandle, JoinSet}; 7 | 8 | pub const PRESETS: &[&str] = &["reboot", "poweroff", "halt", "userspace-reboot"]; 9 | 10 | /// Enter a `reboot`-series milestone. 11 | /// 12 | /// # Panics 13 | /// This function would panic if `name` is not contained in [`PRESETS`]. 14 | pub async fn enter(name: &str) -> Result<(), Error> { 15 | _ = super::enter_milestone(name.into(), &mut HashSet::with_capacity(8)).await; 16 | let reboot_timeout = airupd().storage.config.system_conf.system.reboot_timeout; 17 | stop_all_services(Duration::from_millis(reboot_timeout as _)).await; 18 | 19 | match name { 20 | "reboot" => airupd().lifetime.reboot(), 21 | "poweroff" => airupd().lifetime.poweroff(), 22 | "halt" => airupd().lifetime.halt(), 23 | "userspace-reboot" => airupd().lifetime.userspace_reboot(), 24 | _ => unreachable!(), 25 | } 26 | 27 | Ok(()) 28 | } 29 | 30 | /// Stops all running services. 31 | async fn stop_all_services(timeout: Duration) { 32 | _ = tokio::time::timeout(timeout, async { 33 | let services = airupd().supervisors.list().await; 34 | let mut join_set = JoinSet::new(); 35 | for service in services { 36 | join_set.spawn(stop_service_task(service)); 37 | } 38 | join_set.join_all().await; 39 | }) 40 | .await; 41 | } 42 | 43 | /// Spawns a task to interactively stop a service. 44 | fn stop_service_task(service: String) -> JoinHandle<()> { 45 | tokio::spawn(async move { 46 | let mut error = None; 47 | match airupd().stop_service(&service).await { 48 | Ok(x) => { 49 | if let Err(err) = x.wait().await { 50 | if !matches!(err, Error::NotStarted | Error::Unsupported { message: _ }) { 51 | error = Some(err); 52 | } 53 | } else { 54 | tracing::info!(target: "console", "Stopping {}", super::display_name(&service).await); 55 | } 56 | } 57 | Err(err) => { 58 | if matches!(err, Error::NotFound | Error::NotStarted) { 59 | return; 60 | } 61 | error = Some(err); 62 | } 63 | }; 64 | if let Some(err) = error { 65 | tracing::error!( 66 | target: "console", 67 | "Failed to stop {}: {}", 68 | super::display_name(&service).await, 69 | err 70 | ); 71 | _ = airupd().kill_service(&service).await; 72 | } 73 | _ = airupd().uncache_service(&service).await; 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /airupd/src/rpc/api/debug.rs: -------------------------------------------------------------------------------- 1 | //! APIs that provides Airup debugging utilities. 2 | 3 | use super::{Method, MethodFuture}; 4 | use crate::app::airupd; 5 | use airup_sdk::{ 6 | error::ApiError, 7 | rpc::{Request, Response}, 8 | }; 9 | use std::{collections::HashMap, hash::BuildHasher}; 10 | 11 | pub(super) fn init(methods: &mut HashMap<&'static str, Method, H>) { 12 | crate::ipc_methods!(debug, [echo_raw, dump, exit, is_forking_supervisable,]) 13 | .iter() 14 | .for_each(|(k, v)| { 15 | methods.insert(k, *v); 16 | }); 17 | } 18 | 19 | fn echo_raw(x: Request) -> MethodFuture { 20 | Box::pin(async { 21 | x.extract_params::() 22 | .unwrap_or_else(|x| Response::Err(ApiError::invalid_params(x))) 23 | .into_result() 24 | }) 25 | } 26 | 27 | #[airupfx::macros::api] 28 | async fn dump() -> Result { 29 | Ok(format!("{:#?}", airupd())) 30 | } 31 | 32 | #[airupfx::macros::api] 33 | async fn exit(code: i32) -> Result<(), ApiError> { 34 | airupd().lifetime.exit(code); 35 | Ok(()) 36 | } 37 | 38 | #[airupfx::macros::api] 39 | async fn is_forking_supervisable() -> Result { 40 | Ok(airupfx::process::is_forking_supervisable()) 41 | } 42 | -------------------------------------------------------------------------------- /airupd/src/rpc/api/info.rs: -------------------------------------------------------------------------------- 1 | //! APIs that provides information about Airup and the system. 2 | 3 | use super::{Method, MethodFuture}; 4 | use airup_sdk::{Error, build::BuildManifest}; 5 | use std::{collections::HashMap, hash::BuildHasher}; 6 | 7 | pub(super) fn init(methods: &mut HashMap<&'static str, Method, H>) { 8 | crate::ipc_methods!(info, [version, build_manifest,]) 9 | .iter() 10 | .for_each(|(k, v)| { 11 | methods.insert(k, *v); 12 | }); 13 | } 14 | 15 | #[airupfx::macros::api] 16 | async fn version() -> Result<&'static str, Error> { 17 | Ok(env!("CARGO_PKG_VERSION")) 18 | } 19 | 20 | #[airupfx::macros::api] 21 | async fn build_manifest() -> Result<&'static BuildManifest, Error> { 22 | Ok(airup_sdk::build::manifest()) 23 | } 24 | -------------------------------------------------------------------------------- /airupd/src/rpc/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Airup IPC API - Implementation 2 | 3 | mod debug; 4 | mod info; 5 | pub mod session; 6 | mod system; 7 | 8 | use airup_sdk::{ 9 | Error, 10 | rpc::{Request, Response}, 11 | }; 12 | use airupfx::prelude::*; 13 | use std::collections::HashMap; 14 | 15 | /// The Airup RPC API (implementation) manager. 16 | #[derive(Debug)] 17 | pub struct Manager { 18 | methods: HashMap<&'static str, Method>, 19 | } 20 | impl Manager { 21 | /// Creates a new [`Manager`] instance. 22 | pub fn new() -> Self { 23 | let mut object = Self { 24 | methods: HashMap::with_capacity(32), 25 | }; 26 | object.init(); 27 | object 28 | } 29 | 30 | /// Initializes the [`Manager`] instance with RPC methods. 31 | pub fn init(&mut self) { 32 | info::init(&mut self.methods); 33 | debug::init(&mut self.methods); 34 | system::init(&mut self.methods); 35 | } 36 | 37 | /// Invokes a method by the given request. 38 | pub(super) async fn invoke(&self, req: Request) -> Response { 39 | let method = self.methods.get(&req.method[..]).copied(); 40 | match method { 41 | Some(method) => Response::new(method(req).await), 42 | None => Response::Err(Error::NotImplemented), 43 | } 44 | } 45 | } 46 | impl Default for Manager { 47 | fn default() -> Self { 48 | Self::new() 49 | } 50 | } 51 | 52 | /// Represents to an IPC method. 53 | pub(super) type Method = fn(Request) -> MethodFuture; 54 | 55 | /// Represents to future type of an IPC method. 56 | pub type MethodFuture = BoxFuture<'static, Result>; 57 | 58 | #[macro_export] 59 | macro_rules! ipc_methods { 60 | ($prefix:ident, [$($n:ident),*,]) => { 61 | [ 62 | $((concat!(stringify!($prefix), ".", stringify!($n)), $n as Method)),* 63 | ] 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /airupd/src/rpc/api/session.rs: -------------------------------------------------------------------------------- 1 | //! Session management APIs. 2 | 3 | use crate::app::airupd; 4 | use airup_sdk::rpc::{Request, Response}; 5 | 6 | async fn send_error( 7 | session: &mut crate::rpc::Session, 8 | error: airup_sdk::Error, 9 | ) -> anyhow::Result<()> { 10 | session 11 | .conn 12 | .send(&Response::new(Err::<(), _>(error))) 13 | .await?; 14 | Ok(()) 15 | } 16 | 17 | pub async fn invoke(mut session: crate::rpc::Session, req: Request) { 18 | _ = match &req.method[..] { 19 | "session.into_extension" => into_extension(session, req), 20 | _ => send_error(&mut session, airup_sdk::Error::NotImplemented).await, 21 | }; 22 | } 23 | 24 | fn into_extension(session: crate::rpc::Session, req: Request) -> anyhow::Result<()> { 25 | let name: String = req.extract_params()?; 26 | let conn = session.conn.into_inner().into_inner(); 27 | airupd().extensions.register(name, conn)?; 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /airupd/src/rpc/api/system.rs: -------------------------------------------------------------------------------- 1 | //! APIs that provides system operations. 2 | 3 | use super::{Method, MethodFuture}; 4 | use crate::app::airupd; 5 | use airup_sdk::{ 6 | Error, 7 | files::Service, 8 | system::{Event, QueryService, QuerySystem}, 9 | }; 10 | use std::{collections::HashMap, hash::BuildHasher}; 11 | 12 | pub(super) fn init(methods: &mut HashMap<&'static str, Method, H>) { 13 | crate::ipc_methods!( 14 | system, 15 | [ 16 | refresh, 17 | gc, 18 | start_service, 19 | query_service, 20 | query_system, 21 | stop_service, 22 | kill_service, 23 | reload_service, 24 | sideload_service, 25 | cache_service, 26 | uncache_service, 27 | interrupt_service_task, 28 | list_services, 29 | enter_milestone, 30 | set_instance_name, 31 | trigger_event, 32 | unregister_extension, 33 | ] 34 | ) 35 | .iter() 36 | .for_each(|(k, v)| { 37 | methods.insert(k, *v); 38 | }); 39 | } 40 | 41 | #[airupfx::macros::api] 42 | async fn refresh() -> Result, Error> { 43 | let mut errors = Vec::new(); 44 | 45 | airupfx::env::refresh().await; 46 | for (name, error) in airupd().supervisors.refresh_all().await { 47 | errors.push((format!("service-manifest:{name}"), error)); 48 | } 49 | 50 | Ok(errors) 51 | } 52 | 53 | #[airupfx::macros::api] 54 | async fn gc() -> Result<(), Error> { 55 | airupd().supervisors.gc().await; 56 | Ok(()) 57 | } 58 | 59 | #[airupfx::macros::api] 60 | async fn query_service(service: String) -> Result { 61 | airupd().query_service(&service).await 62 | } 63 | 64 | #[airupfx::macros::api] 65 | async fn query_system() -> Result { 66 | Ok(airupd().query_system().await) 67 | } 68 | 69 | #[airupfx::macros::api] 70 | async fn start_service(service: String) -> Result<(), Error> { 71 | airupd().start_service(&service).await?.wait().await?; 72 | Ok(()) 73 | } 74 | 75 | #[airupfx::macros::api] 76 | async fn stop_service(service: String) -> Result<(), Error> { 77 | airupd().stop_service(&service).await?.wait().await?; 78 | Ok(()) 79 | } 80 | 81 | #[airupfx::macros::api] 82 | async fn kill_service(service: String) -> Result<(), Error> { 83 | airupd().kill_service(&service).await 84 | } 85 | 86 | #[airupfx::macros::api] 87 | async fn reload_service(service: String) -> Result<(), Error> { 88 | airupd().reload_service(&service).await?.wait().await?; 89 | Ok(()) 90 | } 91 | 92 | #[airupfx::macros::api] 93 | async fn interrupt_service_task(service: String) -> Result<(), Error> { 94 | airupd() 95 | .interrupt_service_task(&service) 96 | .await? 97 | .wait() 98 | .await 99 | .map(|_| ()) 100 | } 101 | 102 | #[airupfx::macros::api] 103 | async fn sideload_service(name: String, service: Service) -> Result<(), Error> { 104 | airupd().sideload_service(&name, service).await 105 | } 106 | 107 | #[airupfx::macros::api] 108 | async fn cache_service(service: String) -> Result<(), Error> { 109 | airupd().cache_service(&service).await 110 | } 111 | 112 | #[airupfx::macros::api] 113 | async fn uncache_service(service: String) -> Result<(), Error> { 114 | airupd().uncache_service(&service).await 115 | } 116 | 117 | #[airupfx::macros::api] 118 | async fn list_services() -> Result, Error> { 119 | Ok(airupd().storage.services.list().await) 120 | } 121 | 122 | #[airupfx::macros::api] 123 | async fn enter_milestone(name: String) -> Result<(), Error> { 124 | airupd().enter_milestone(name).await 125 | } 126 | 127 | #[airupfx::macros::api] 128 | async fn set_instance_name(name: String) -> Result<(), Error> { 129 | airupfx::env::set_instance_name(name); 130 | Ok(()) 131 | } 132 | 133 | #[airupfx::macros::api] 134 | async fn trigger_event(event: Event) -> Result<(), Error> { 135 | airupd().events.trigger(event).await; 136 | Ok(()) 137 | } 138 | 139 | #[airupfx::macros::api] 140 | async fn unregister_extension(name: String) -> Result<(), Error> { 141 | airupd().extensions.unregister(&name) 142 | } 143 | -------------------------------------------------------------------------------- /airupd/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Airup IPC - Server Implementation 2 | 3 | pub mod api; 4 | 5 | use crate::app::airupd; 6 | use airup_sdk::rpc::Request; 7 | use std::path::PathBuf; 8 | use tokio::sync::broadcast; 9 | 10 | /// An instance of the Airup IPC context. 11 | #[derive(Debug)] 12 | pub struct Context { 13 | api: api::Manager, 14 | reload: broadcast::Sender<()>, 15 | } 16 | impl Context { 17 | /// Creates a new `Context` instance. 18 | pub fn new() -> Self { 19 | Self { 20 | api: api::Manager::new(), 21 | reload: broadcast::channel(1).0, 22 | } 23 | } 24 | 25 | pub fn reload(&self) { 26 | _ = self.reload.send(()); 27 | } 28 | } 29 | impl Default for Context { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | /// Represents to an RPC server. 36 | #[derive(Debug)] 37 | pub struct Server { 38 | path: Option, 39 | server: airup_sdk::nonblocking::rpc::Server, 40 | } 41 | impl Server { 42 | /// Creates a new [`Server`] instance. 43 | pub async fn with_path>(path: P) -> anyhow::Result { 44 | let path = path.into(); 45 | let server = airup_sdk::nonblocking::rpc::Server::new(&path)?; 46 | airupfx::fs::set_permission(&path, airupfx::fs::Permission::Socket).await?; 47 | 48 | Ok(Self { 49 | path: Some(path), 50 | server, 51 | }) 52 | } 53 | 54 | /// Forces to create a new [`Server`] instance. 55 | pub async fn with_path_force>(path: P) -> anyhow::Result { 56 | let path = path.into(); 57 | _ = tokio::fs::remove_file(&path).await; 58 | 59 | Self::with_path(path).await 60 | } 61 | 62 | /// Starts the server task. 63 | pub fn start(mut self) { 64 | tokio::spawn(async move { 65 | self.run().await; 66 | }); 67 | } 68 | 69 | /// Reloads the server. 70 | async fn reload(&mut self) -> anyhow::Result<()> { 71 | if let Some(path) = self.path.as_ref() { 72 | let newer = Self::with_path_force(path).await?; 73 | *self = newer; 74 | } 75 | Ok(()) 76 | } 77 | 78 | /// Runs the server in place. 79 | async fn run(&mut self) { 80 | let mut reload = airupd().rpc.reload.subscribe(); 81 | 82 | loop { 83 | tokio::select! { 84 | Ok(()) = reload.recv() => { 85 | _ = self.reload().await; 86 | }, 87 | Ok(conn) = self.server.accept() => { 88 | Session::new(conn).start(); 89 | }, 90 | }; 91 | } 92 | } 93 | } 94 | 95 | /// Represents to an Airupd IPC session. 96 | #[derive(Debug)] 97 | pub struct Session { 98 | conn: airup_sdk::nonblocking::rpc::Connection, 99 | } 100 | impl Session { 101 | /// Constructs a new [`Session`] instance with connection `conn`. 102 | fn new(conn: airup_sdk::nonblocking::rpc::Connection) -> Self { 103 | Self { conn } 104 | } 105 | 106 | /// Starts the session task. 107 | fn start(self) { 108 | tokio::spawn(async move { 109 | _ = self.run().await; 110 | }); 111 | } 112 | 113 | /// Runs the session in place. 114 | async fn run(mut self) -> anyhow::Result<()> { 115 | loop { 116 | let req = self.conn.recv_req().await?; 117 | if req.method.strip_prefix("session.").is_some() { 118 | api::session::invoke(self, req).await; 119 | return Ok(()); 120 | } 121 | let resp = match req.method.strip_prefix("extapi.") { 122 | Some(method) => airupd() 123 | .extensions 124 | .rpc_invoke(Request::new::<&str, ciborium::Value, _>(method, req.params)) 125 | .await 126 | .unwrap(), 127 | None => airupd().rpc.api.invoke(req).await, 128 | }; 129 | self.conn.send(&resp).await?; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /airupd/src/storage/config.rs: -------------------------------------------------------------------------------- 1 | //! Represents to Airup's config directory. 2 | 3 | use airup_sdk::{files::SystemConf, prelude::*}; 4 | use std::{collections::HashMap, path::PathBuf}; 5 | 6 | /// Main navigator of Airup's config directory. 7 | #[derive(Debug)] 8 | pub struct Config { 9 | pub base_dir: DirChain<'static>, 10 | pub system_conf: SystemConf, 11 | } 12 | impl Config { 13 | /// Creates a new [`Config`] instance. 14 | pub async fn new() -> Self { 15 | let base_dir = airup_sdk::build::manifest().config_dir.clone(); 16 | let system_conf = SystemConf::read_from(&base_dir.join("system.airc")) 17 | .await 18 | .unwrap_or_default(); 19 | 20 | Self { 21 | base_dir: base_dir.into(), 22 | system_conf, 23 | } 24 | } 25 | 26 | /// Returns path of separated config file for specified service. 27 | pub async fn of_service(&self, name: &str) -> Option { 28 | let name = name.strip_suffix(".airs").unwrap_or(name); 29 | self.base_dir.find(format!("{name}.airs.airc")).await 30 | } 31 | 32 | /// Populates the process' environment with the system config. 33 | pub fn populate_system_config(&self) { 34 | if !self.system_conf.system.instance_name.is_empty() { 35 | airupfx::env::set_instance_name(self.system_conf.system.instance_name.clone()); 36 | } 37 | 38 | let mut vars: HashMap> = HashMap::default(); 39 | for (k, v) in &airup_sdk::build::manifest().env_vars { 40 | vars.insert(k.to_owned(), v.as_ref().map(Into::into)); 41 | } 42 | for (k, v) in &self.system_conf.env.vars { 43 | if v.as_integer() == Some(0) { 44 | vars.insert(k.clone(), None); 45 | } else if let Some(s) = v.as_str() { 46 | vars.insert(k.clone(), Some(s.into())); 47 | } 48 | } 49 | airupfx::env::set_vars(vars); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /airupd/src/storage/milestones.rs: -------------------------------------------------------------------------------- 1 | //! Represents to Airup's milestone directory. 2 | 3 | use airup_sdk::{ 4 | files::{Milestone, ReadError}, 5 | prelude::*, 6 | }; 7 | 8 | /// Represents to Airup's milestones directory. 9 | #[derive(Debug)] 10 | pub struct Milestones { 11 | base_chain: DirChain<'static>, 12 | } 13 | impl From> for Milestones { 14 | fn from(val: DirChain<'static>) -> Self { 15 | Self { base_chain: val } 16 | } 17 | } 18 | impl Milestones { 19 | /// Creates a new [`Milestones`] instance. 20 | pub fn new() -> Self { 21 | Self { 22 | base_chain: DirChain::new(airup_sdk::build::manifest().milestone_dir.clone()), 23 | } 24 | } 25 | 26 | /// Attempts to find and parse a milestone. 27 | pub async fn get(&self, name: &str) -> Result { 28 | let name = name.strip_suffix(".airm").unwrap_or(name); 29 | match self.base_chain.find(format!("{name}.airm")).await { 30 | Some(x) => Milestone::read_from(x).await, 31 | None => Err(std::io::ErrorKind::NotFound.into()), 32 | } 33 | } 34 | } 35 | impl Default for Milestones { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /airupd/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! Storage subsystem of the Airup daemon. 2 | 3 | mod config; 4 | mod milestones; 5 | mod runtime; 6 | mod services; 7 | 8 | use self::config::Config; 9 | use self::milestones::Milestones; 10 | use self::runtime::Runtime; 11 | use self::services::Services; 12 | use airup_sdk::files::{ReadError, Service}; 13 | 14 | /// Main navigator of Airup's storage. 15 | #[derive(Debug)] 16 | pub struct Storage { 17 | pub config: Config, 18 | pub runtime: Runtime, 19 | pub services: Services, 20 | pub milestones: Milestones, 21 | } 22 | impl Storage { 23 | /// Creates a new [`Storage`] instance. 24 | pub async fn new() -> Self { 25 | Self { 26 | config: Config::new().await, 27 | runtime: Runtime::new().await, 28 | services: Services::new(), 29 | milestones: Milestones::new(), 30 | } 31 | } 32 | 33 | /// Gets a patched service installation. 34 | pub async fn get_service_patched(&self, name: &str) -> Result { 35 | let patch = self.config.of_service(name).await; 36 | self.services.get_and_patch(name, patch).await 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /airupd/src/storage/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Represents to Airup's runtime directory. 2 | 3 | use crate::rpc; 4 | use airupfx::fs::Lock; 5 | use std::path::PathBuf; 6 | 7 | /// Main navigator of Airup's runtime directory. 8 | #[derive(Debug)] 9 | pub struct Runtime { 10 | base_dir: PathBuf, 11 | } 12 | impl Runtime { 13 | /// Creates a new [`Runtime`] instance. 14 | pub async fn new() -> Self { 15 | let base_dir = airup_sdk::build::manifest().runtime_dir.clone(); 16 | _ = tokio::fs::create_dir_all(&base_dir).await; 17 | 18 | Self { base_dir } 19 | } 20 | 21 | /// Locks airup data. 22 | pub async fn lock(&self) -> std::io::Result { 23 | Lock::new(self.base_dir.join("airupd.lock")).await 24 | } 25 | 26 | /// Creates an IPC server. 27 | pub async fn ipc_server(&self) -> anyhow::Result { 28 | let socket_path = self.base_dir.join("airupd.sock"); 29 | 30 | // FIXME: Should we avoid using `std::env::set_var` here? 31 | unsafe { 32 | std::env::set_var("AIRUP_SOCK", &socket_path); 33 | } 34 | 35 | rpc::Server::with_path_force(&socket_path).await 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /airupd/src/storage/services.rs: -------------------------------------------------------------------------------- 1 | //! Represents to Airup's services directory. 2 | 3 | use airup_sdk::{ 4 | files::{ReadError, Service}, 5 | nonblocking::files, 6 | prelude::*, 7 | }; 8 | use std::path::PathBuf; 9 | 10 | /// Represents to Airup's services directory. 11 | #[derive(Debug)] 12 | pub struct Services { 13 | base_chain: DirChain<'static>, 14 | } 15 | impl From> for Services { 16 | fn from(val: DirChain<'static>) -> Self { 17 | Self { base_chain: val } 18 | } 19 | } 20 | impl Services { 21 | /// Creates a new [`Services`] instance. 22 | pub fn new() -> Self { 23 | Self { 24 | base_chain: DirChain::new(airup_sdk::build::manifest().service_dir.clone()), 25 | } 26 | } 27 | 28 | /// Returns path of the specified service. 29 | pub async fn get_and_patch( 30 | &self, 31 | name: &str, 32 | patch: Option, 33 | ) -> Result { 34 | let name = name.strip_suffix(".airs").unwrap_or(name); 35 | 36 | let main_path = self 37 | .base_chain 38 | .find(format!("{name}.airs")) 39 | .await 40 | .ok_or_else(|| ReadError::from(std::io::ErrorKind::NotFound))?; 41 | 42 | let mut paths = Vec::with_capacity(2); 43 | paths.push(main_path); 44 | if let Some(path) = patch { 45 | paths.push(path); 46 | } 47 | 48 | files::read_merge(paths).await 49 | } 50 | 51 | /// Lists names of all services installed in the storage. 52 | pub async fn list(&self) -> Vec { 53 | self.base_chain 54 | .read_chain() 55 | .await 56 | .map(IntoIterator::into_iter) 57 | .into_iter() 58 | .flatten() 59 | .map(|x| String::from(x.to_string_lossy())) 60 | .filter(|x| !x.starts_with('.') && x.ends_with(".airs")) 61 | .map(|x| x.strip_suffix(".airs").unwrap_or(&x).into()) 62 | .collect() 63 | } 64 | } 65 | impl Default for Services { 66 | fn default() -> Self { 67 | Self::new() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /airupd/src/supervisor/task/cleanup.rs: -------------------------------------------------------------------------------- 1 | //! # The `CleanupService` Task 2 | //! This is started when the service stopped. This executes the cleanup command, and (if necessary) restarts the service. 3 | //! Depending on what it is doing, it may act as `StartService` or `StopService`. 4 | 5 | use super::*; 6 | use crate::ace::CommandExitError; 7 | use airup_sdk::files::Service; 8 | use airupfx::{prelude::*, process::Wait}; 9 | use std::{ 10 | sync::{ 11 | Arc, 12 | atomic::{self, AtomicBool}, 13 | }, 14 | time::Duration, 15 | }; 16 | 17 | #[derive(Debug)] 18 | pub struct CleanupServiceHandle { 19 | helper: TaskHelperHandle, 20 | important: Arc, 21 | retry: bool, 22 | } 23 | impl TaskHandle for CleanupServiceHandle { 24 | fn task_class(&self) -> &'static str { 25 | match self.retry { 26 | true => "StartService", 27 | false => "StopService", 28 | } 29 | } 30 | 31 | fn is_important(&self) -> bool { 32 | self.important.load(atomic::Ordering::Acquire) 33 | } 34 | 35 | fn send_interrupt(&self) { 36 | self.helper.send_interrupt() 37 | } 38 | 39 | fn wait(&self) -> BoxFuture> { 40 | self.helper.wait() 41 | } 42 | } 43 | 44 | pub(in crate::supervisor) fn start( 45 | context: Arc, 46 | wait: Wait, 47 | ) -> Arc { 48 | let (handle, helper) = task_helper(); 49 | let important: Arc = AtomicBool::new(true).into(); 50 | 51 | let retry_cond1 = context.service.watchdog.successful_exit || !wait.is_success(); 52 | let retry_cond2 = context 53 | .retry 54 | .check_and_mark(context.service.retry.max_attempts); 55 | let retry = retry_cond1 && retry_cond2; 56 | 57 | let cleanup_service = CleanupService { 58 | helper, 59 | context, 60 | important: important.clone(), 61 | retry, 62 | wait, 63 | }; 64 | cleanup_service.start(); 65 | 66 | Arc::new(CleanupServiceHandle { 67 | helper: handle, 68 | important, 69 | retry, 70 | }) 71 | } 72 | 73 | #[derive(Debug)] 74 | struct CleanupService { 75 | helper: TaskHelper, 76 | context: Arc, 77 | important: Arc, 78 | retry: bool, 79 | wait: Wait, 80 | } 81 | impl CleanupService { 82 | fn start(mut self) { 83 | tokio::spawn(async move { 84 | let val = self.run().await; 85 | self.helper.finish(val); 86 | }); 87 | } 88 | 89 | async fn run(&mut self) -> Result<(), Error> { 90 | let ace = super::ace(&self.context).await?; 91 | 92 | _ = cleanup_service( 93 | &ace, 94 | &self.context.service, 95 | &airupfx::time::countdown(self.context.service.exec.stop_timeout()), 96 | ) 97 | .await; 98 | 99 | self.important.store(false, atomic::Ordering::Release); 100 | self.helper 101 | .would_interrupt(async { 102 | tokio::time::sleep(Duration::from_millis(self.context.service.retry.delay)).await; 103 | }) 104 | .await?; 105 | 106 | if self.retry { 107 | self.important.store(true, atomic::Ordering::Release); 108 | let handle = super::start::start(self.context.clone()); 109 | tokio::select! { 110 | _ = handle.wait() => {}, 111 | _ = self.helper.interrupt_flag.wait_for(|x| *x) => { 112 | handle.send_interrupt(); 113 | }, 114 | }; 115 | } else if self.context.retry.enabled() && self.context.service.watchdog.successful_exit { 116 | self.context 117 | .last_error 118 | .set::(CommandExitError::from_wait_force(&self.wait).into()); 119 | } 120 | 121 | Ok(()) 122 | } 123 | } 124 | 125 | pub async fn cleanup_service( 126 | ace: &Ace, 127 | service: &Service, 128 | countdown: &airupfx::time::Countdown, 129 | ) -> Result<(), Error> { 130 | if let Some(x) = &service.service.pid_file { 131 | _ = tokio::fs::remove_file(x).await; 132 | } 133 | 134 | if let Some(x) = &service.exec.post_stop { 135 | for line in x.lines() { 136 | ace.run_wait_timeout(line.trim(), countdown.left()) 137 | .await??; 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | -------------------------------------------------------------------------------- /airupd/src/supervisor/task/health_check.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::ace::CommandExitError; 3 | use airupfx::prelude::*; 4 | use std::{sync::Arc, time::Duration}; 5 | 6 | #[derive(Debug)] 7 | pub struct HealthCheckHandle { 8 | helper: TaskHelperHandle, 9 | } 10 | impl TaskHandle for HealthCheckHandle { 11 | fn task_class(&self) -> &'static str { 12 | "HealthCheck" 13 | } 14 | 15 | fn is_important(&self) -> bool { 16 | false 17 | } 18 | 19 | fn send_interrupt(&self) { 20 | self.helper.send_interrupt() 21 | } 22 | 23 | fn wait(&self) -> BoxFuture> { 24 | self.helper.wait() 25 | } 26 | } 27 | 28 | pub(in crate::supervisor) async fn start(context: &SupervisorContext) -> Arc { 29 | let (handle, helper) = task_helper(); 30 | let command = context.service.exec.health_check.clone(); 31 | let timeout = context.service.exec.health_check_timeout(); 32 | 33 | let reload_service = HealthCheck { 34 | helper, 35 | ace: super::ace(context).await, 36 | command, 37 | timeout, 38 | }; 39 | reload_service.start(); 40 | 41 | Arc::new(HealthCheckHandle { helper: handle }) 42 | } 43 | 44 | struct HealthCheck { 45 | helper: TaskHelper, 46 | ace: Result, 47 | command: Option, 48 | timeout: Option, 49 | } 50 | impl HealthCheck { 51 | fn start(mut self) { 52 | tokio::spawn(async move { 53 | let val = self.run().await; 54 | self.helper.finish(val); 55 | }); 56 | } 57 | 58 | async fn run(&mut self) -> Result<(), Error> { 59 | let ace = std::mem::replace(&mut self.ace, Err(Error::internal("taken ace")))?; 60 | self.helper 61 | .would_interrupt(async { 62 | if let Some(x) = &self.command { 63 | ace.run_wait_timeout(x, self.timeout).await??; 64 | } 65 | Ok::<_, Error>(Ok::<_, CommandExitError>(())) 66 | }) 67 | .await???; 68 | Ok(()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /airupd/src/supervisor/task/reload.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::ace::CommandExitError; 3 | use airup_sdk::prelude::*; 4 | use airupfx::prelude::*; 5 | use std::{sync::Arc, time::Duration}; 6 | 7 | #[derive(Debug)] 8 | pub struct ReloadServiceHandle { 9 | helper: TaskHelperHandle, 10 | } 11 | impl TaskHandle for ReloadServiceHandle { 12 | fn task_class(&self) -> &'static str { 13 | "ReloadService" 14 | } 15 | 16 | fn is_important(&self) -> bool { 17 | false 18 | } 19 | 20 | fn send_interrupt(&self) { 21 | self.helper.send_interrupt() 22 | } 23 | 24 | fn wait(&self) -> BoxFuture> { 25 | self.helper.wait() 26 | } 27 | } 28 | 29 | pub(in crate::supervisor) async fn start(context: &SupervisorContext) -> Arc { 30 | let (handle, helper) = task_helper(); 31 | 32 | let reload_service = ReloadService { 33 | helper, 34 | ace: super::ace(context).await, 35 | status: context.status.get(), 36 | reload_cmd: context.service.exec.reload.clone(), 37 | reload_timeout: context.service.exec.reload_timeout(), 38 | }; 39 | reload_service.start(); 40 | 41 | Arc::new(ReloadServiceHandle { helper: handle }) 42 | } 43 | 44 | struct ReloadService { 45 | helper: TaskHelper, 46 | ace: Result, 47 | status: Status, 48 | reload_cmd: Option, 49 | reload_timeout: Option, 50 | } 51 | impl ReloadService { 52 | fn start(mut self) { 53 | tokio::spawn(async move { 54 | let val = self.run().await; 55 | self.helper.finish(val); 56 | }); 57 | } 58 | 59 | async fn run(&mut self) -> Result<(), Error> { 60 | if self.status != Status::Active { 61 | return Err(Error::NotStarted); 62 | } 63 | 64 | let ace = std::mem::replace(&mut self.ace, Err(Error::internal("taken ace")))?; 65 | 66 | self.helper 67 | .would_interrupt(async { 68 | if let Some(reload_cmd) = &self.reload_cmd { 69 | ace.run_wait_timeout(reload_cmd, self.reload_timeout) 70 | .await??; 71 | } 72 | Ok::<_, Error>(Ok::<_, CommandExitError>(())) 73 | }) 74 | .await???; 75 | 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /airupd/src/supervisor/task/stop.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use airup_sdk::system::Status; 3 | use airupfx::signal::SIGTERM; 4 | use std::sync::Arc; 5 | 6 | #[derive(Debug)] 7 | pub struct StopServiceHandle { 8 | helper: TaskHelperHandle, 9 | } 10 | impl TaskHandle for StopServiceHandle { 11 | fn task_class(&self) -> &'static str { 12 | "StopService" 13 | } 14 | 15 | fn is_important(&self) -> bool { 16 | true 17 | } 18 | 19 | fn send_interrupt(&self) { 20 | self.helper.send_interrupt() 21 | } 22 | 23 | fn wait(&self) -> BoxFuture> { 24 | self.helper.wait() 25 | } 26 | } 27 | 28 | pub(in crate::supervisor) fn start(context: Arc) -> Arc { 29 | let (handle, helper) = task_helper(); 30 | 31 | let stop_service = StopService { helper, context }; 32 | stop_service.start(); 33 | 34 | Arc::new(StopServiceHandle { helper: handle }) 35 | } 36 | 37 | #[derive(Debug)] 38 | struct StopService { 39 | helper: TaskHelper, 40 | context: Arc, 41 | } 42 | impl StopService { 43 | fn start(mut self) { 44 | tokio::spawn(async move { 45 | let val = self.run().await; 46 | self.helper.finish(val); 47 | }); 48 | } 49 | 50 | async fn run(&mut self) -> Result<(), Error> { 51 | // The task immediately fails if the service is not active 52 | if self.context.status.get() != Status::Active { 53 | return Err(Error::NotStarted); 54 | } 55 | 56 | // Auto saving of last error is disabled for this task 57 | self.context.last_error.set(None); 58 | 59 | let ace = super::ace(&self.context).await?; 60 | let countdown = airupfx::time::countdown(self.context.service.exec.stop_timeout()); 61 | 62 | if let Some(x) = &self.context.service.exec.pre_stop { 63 | for line in x.lines() { 64 | ace.run_wait_timeout(line.trim(), countdown.left()) 65 | .await??; 66 | } 67 | } 68 | 69 | match &self.context.service.exec.stop { 70 | Some(x) => { 71 | ace.run_wait_timeout(x, countdown.left()).await??; 72 | } 73 | None => { 74 | if let Some(x) = self.context.child.read().await.as_ref() { 75 | x.kill_timeout(SIGTERM, countdown.left()).await?; 76 | } else { 77 | return Err(Error::unsupported("this service cannot be stopped")); 78 | } 79 | } 80 | }; 81 | 82 | self.context.status.set(Status::Stopped); 83 | 84 | _ = super::cleanup::cleanup_service(&ace, &self.context.service, &countdown).await; 85 | 86 | Ok(()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /airupfx/airupfx-env/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-env" 3 | authors = ["sisungo "] 4 | version = "0.6.1" 5 | edition = "2021" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | cfg-if = "1" 13 | sysinfo = "0.35" 14 | tokio = { workspace = true } 15 | 16 | [target.'cfg(target_family = "unix")'.dependencies] 17 | libc = "0.2" 18 | -------------------------------------------------------------------------------- /airupfx/airupfx-env/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Inspection and manipulation of the process's environment. 2 | 3 | pub mod users; 4 | 5 | pub use users::{current_uid, with_current_user, with_user_by_id, with_user_by_name}; 6 | 7 | use std::{ 8 | ffi::{OsStr, OsString}, 9 | path::Path, 10 | sync::RwLock, 11 | }; 12 | 13 | static INSTANCE_NAME: RwLock = RwLock::new(String::new()); 14 | 15 | /// Sets environment variables in the iterator for the currently running process, removing environment variables with value 16 | /// `None`. 17 | /// 18 | /// # Panics 19 | /// This function may panic if any of the keys is empty, contains an ASCII equals sign '=' or the NUL character '\0', or when 20 | /// the value contains the NUL character. 21 | #[inline] 22 | pub fn set_vars)>, K: Into, V: Into>( 23 | iter: I, 24 | ) { 25 | iter.into_iter().for_each(|(k, v)| match v { 26 | Some(x) => std::env::set_var(k.into(), x.into()), 27 | None => std::env::remove_var(k.into()), 28 | }); 29 | } 30 | 31 | /// Fetches the environment variable key from the current process, then removes the environment variable from the environment 32 | /// of current process. 33 | /// 34 | /// # Panics 35 | /// This function may panic if key is empty, contains an ASCII equals sign '=' or the NUL character '\0', or when value contains 36 | /// the NUL character. 37 | /// 38 | /// # Errors 39 | /// An `Err(_)` is returned if the specific variable is not existing. 40 | #[inline] 41 | pub fn take_var>(key: K) -> Result { 42 | let value = std::env::var(key.as_ref())?; 43 | std::env::remove_var(key); 44 | Ok(value) 45 | } 46 | 47 | /// Refreshes the environmental database. 48 | #[inline] 49 | pub async fn refresh() { 50 | users::refresh(); 51 | } 52 | 53 | /// Sets instance name of the process. 54 | #[inline] 55 | pub fn set_instance_name(name: String) { 56 | *INSTANCE_NAME.write().unwrap() = name; 57 | } 58 | 59 | /// Returns instance name of the process. This is default to the machine's host name, and can be set via [`set_instance_name`]. 60 | #[inline] 61 | pub fn instance_name() -> String { 62 | let global = INSTANCE_NAME.read().unwrap(); 63 | if global.is_empty() { 64 | sysinfo::System::host_name().unwrap_or_else(|| String::from("localhost")) 65 | } else { 66 | global.clone() 67 | } 68 | } 69 | 70 | pub async fn setup_stdio(path: &Path) -> std::io::Result<()> { 71 | cfg_if::cfg_if! { 72 | if #[cfg(target_family = "unix")] { 73 | use std::os::unix::io::AsRawFd; 74 | 75 | loop { 76 | let file = tokio::fs::File::options() 77 | .read(true) 78 | .write(true) 79 | .open(path) 80 | .await?; 81 | if file.as_raw_fd() >= 3 { 82 | break Ok(()); 83 | } else { 84 | std::mem::forget(file); 85 | } 86 | } 87 | } else { 88 | Ok(()) 89 | } 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | #[test] 96 | fn take_var() { 97 | std::env::set_var("magic", "1"); 98 | assert!(matches!(std::env::var("magic").as_deref(), Ok("1"))); 99 | assert!(matches!(crate::take_var("magic").as_deref(), Ok("1"))); 100 | assert!(matches!(std::env::var("magic").as_deref(), Err(_))); 101 | } 102 | 103 | #[test] 104 | fn instance_name() { 105 | let x = crate::instance_name(); 106 | crate::set_instance_name(format!("{x}-testing")); 107 | let y = crate::instance_name(); 108 | assert_ne!(x, y); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /airupfx/airupfx-env/src/users.rs: -------------------------------------------------------------------------------- 1 | //! Inspection and manipulation of the operating system's multi-user function. 2 | 3 | use std::sync::{OnceLock, RwLock}; 4 | use sysinfo::{Uid, User}; 5 | 6 | fn sysinfo_users() -> &'static RwLock { 7 | static USERS: OnceLock> = OnceLock::new(); 8 | USERS.get_or_init(|| sysinfo::Users::new_with_refreshed_list().into()) 9 | } 10 | 11 | /// Refreshes users database. 12 | pub fn refresh() { 13 | sysinfo_users().write().unwrap().refresh(); 14 | } 15 | 16 | /// Finds a user entry by UID. 17 | pub fn with_user_by_id T, T>(uid: &Uid, f: F) -> Option { 18 | Some(f(sysinfo_users() 19 | .read() 20 | .unwrap() 21 | .iter() 22 | .find(|u| u.id() == uid)?)) 23 | } 24 | 25 | /// Finds a user entry by username. 26 | pub fn with_user_by_name T, T>(name: &str, f: F) -> Option { 27 | Some(f(sysinfo_users() 28 | .read() 29 | .unwrap() 30 | .iter() 31 | .find(|u| u.name() == name)?)) 32 | } 33 | 34 | /// Returns the user entry of current user. 35 | pub fn with_current_user T, T>(f: F) -> Option { 36 | with_user_by_id(¤t_uid(), f) 37 | } 38 | 39 | /// Returns UID of current user. 40 | pub fn current_uid() -> Uid { 41 | cfg_if::cfg_if! { 42 | if #[cfg(target_family = "unix")] { 43 | Uid::try_from(unsafe { libc::getuid() } as usize).unwrap() 44 | } else { 45 | std::compile_error!("This target is not supported by `Airup` yet. Consider opening an issue at https://github.com/sisungo/airup/issues?"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /airupfx/airupfx-extensions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-extensions" 3 | authors = ["sisungo "] 4 | version = "0.10.6" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | [dependencies] 10 | anyhow = "1" 11 | airup-sdk = { path = "../../airup-sdk" } 12 | airupfx-fs = { path = "../airupfx-fs" } 13 | airupfx-signal = { path = "../airupfx-signal" } 14 | ciborium = "0.2" 15 | tokio = { workspace = true } 16 | -------------------------------------------------------------------------------- /airupfx/airupfx-fs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-fs" 3 | authors = ["sisungo "] 4 | version = "0.6.1" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | cfg-if = "1" 13 | tokio = { workspace = true } 14 | 15 | [target.'cfg(target_family = "unix")'.dependencies] 16 | libc = "0.2" 17 | -------------------------------------------------------------------------------- /airupfx/airupfx-fs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Filesystem utilities. 2 | 3 | cfg_if::cfg_if! { 4 | if #[cfg(target_family = "unix")] { 5 | #[path = "unix.rs"] 6 | mod sys; 7 | } else { 8 | std::compile_error!( 9 | "This target is not supported by `Airup` yet. Consider opening an issue at https://github.com/sisungo/airup/issues?" 10 | ); 11 | } 12 | } 13 | 14 | use std::path::{Path, PathBuf}; 15 | use tokio::io::AsyncWriteExt; 16 | 17 | /// Represents to a file permission. 18 | #[derive(Debug, Clone, Copy)] 19 | pub enum Permission { 20 | /// Permissions for socket files. 21 | /// 22 | /// Socket files should only be accessed by current user, or if we are `pid == 1`, the `airup` group. 23 | Socket, 24 | 25 | /// Permissions for lock files. 26 | /// 27 | /// Lock files should always be read, but never written. 28 | Lock, 29 | } 30 | 31 | pub async fn set_permission>(path: P, perm: Permission) -> std::io::Result<()> { 32 | sys::set_permission(path.as_ref(), perm).await 33 | } 34 | 35 | /// Represents to a lock file. 36 | #[derive(Debug)] 37 | pub struct Lock { 38 | holder: Option, 39 | path: PathBuf, 40 | } 41 | impl Lock { 42 | /// Creates an owned [`Lock`] instance for specified path. 43 | pub async fn new(path: PathBuf) -> std::io::Result { 44 | let mut options = tokio::fs::File::options(); 45 | options.write(true); 46 | if std::process::id() != 1 { 47 | options.create_new(true); 48 | } else { 49 | options.create(true).truncate(true); 50 | } 51 | 52 | let mut holder = options.open(&path).await?; 53 | holder 54 | .write_all(std::process::id().to_string().as_bytes()) 55 | .await?; 56 | _ = set_permission(&path, Permission::Lock).await; 57 | 58 | Ok(Self { 59 | holder: Some(holder.into_std().await), 60 | path, 61 | }) 62 | } 63 | } 64 | impl Drop for Lock { 65 | fn drop(&mut self) { 66 | drop(self.holder.take()); 67 | _ = std::fs::remove_file(&self.path); 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | #[tokio::test] 74 | async fn lock() { 75 | let path = std::path::Path::new("/tmp/.airupfx-fs.test.lock"); 76 | _ = tokio::fs::remove_file(path).await; 77 | let lock = crate::Lock::new(path.into()).await.unwrap(); 78 | assert_eq!( 79 | tokio::fs::read_to_string(path).await.unwrap(), 80 | std::process::id().to_string() 81 | ); 82 | drop(lock); 83 | assert!(!path.exists()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /airupfx/airupfx-fs/src/unix.rs: -------------------------------------------------------------------------------- 1 | use crate::Permission; 2 | use std::{fs::Permissions, os::unix::prelude::PermissionsExt, path::Path}; 3 | 4 | pub async fn set_permission(path: &Path, perm: Permission) -> std::io::Result<()> { 5 | match perm { 6 | Permission::Socket => set_sock_permission(path).await, 7 | Permission::Lock => set_lock_permission(path).await, 8 | } 9 | } 10 | 11 | async fn set_sock_permission(path: &Path) -> std::io::Result<()> { 12 | tokio::fs::set_permissions(path, Permissions::from_mode(0o700)).await 13 | } 14 | 15 | async fn set_lock_permission(path: &Path) -> std::io::Result<()> { 16 | tokio::fs::set_permissions(path, Permissions::from_mode(0o444)).await 17 | } 18 | -------------------------------------------------------------------------------- /airupfx/airupfx-io/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-io" 3 | authors = ["sisungo "] 4 | version = "0.10.4" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | tokio = { workspace = true } 13 | -------------------------------------------------------------------------------- /airupfx/airupfx-io/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! IO toolkit. 2 | 3 | pub mod line_piper; 4 | 5 | pub use line_piper::LinePiper; 6 | -------------------------------------------------------------------------------- /airupfx/airupfx-isolator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-isolator" 3 | authors = ["sisungo "] 4 | version = "0.8.0" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | cgroups = ["dep:cgroups-rs"] 13 | 14 | [dependencies] 15 | airupfx-time = { path = "../airupfx-time" } 16 | cfg-if = "1" 17 | 18 | [target.'cfg(target_os = "linux")'.dependencies] 19 | cgroups-rs = { version = "0.3", optional = true } 20 | -------------------------------------------------------------------------------- /airupfx/airupfx-isolator/src/fallback.rs: -------------------------------------------------------------------------------- 1 | //! A fallback isolator implementation that does no-op for all operations. 2 | //! 3 | //! This is useful for compatibility with operating systems that support no isolators. 4 | 5 | #[derive(Debug)] 6 | pub struct Realm; 7 | impl Realm { 8 | pub fn new() -> std::io::Result { 9 | Err(std::io::ErrorKind::Unsupported.into()) 10 | } 11 | 12 | pub fn set_cpu_limit(&self, _: u64) -> std::io::Result<()> { 13 | Ok(()) 14 | } 15 | 16 | pub fn set_mem_limit(&self, _: usize) -> std::io::Result<()> { 17 | Ok(()) 18 | } 19 | 20 | pub fn add(&self, _: i64) -> std::io::Result<()> { 21 | Ok(()) 22 | } 23 | 24 | pub fn kill(&self) -> std::io::Result<()> { 25 | Ok(()) 26 | } 27 | 28 | pub fn memory_usage(&self) -> std::io::Result { 29 | Ok(0) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /airupfx/airupfx-isolator/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for handling resource isolation. 2 | 3 | cfg_if::cfg_if! { 4 | if #[cfg(all(target_os = "linux", feature = "cgroups"))] { 5 | #[path = "linux.rs"] 6 | mod sys; 7 | } else { 8 | #[path = "fallback.rs"] 9 | mod sys; 10 | } 11 | } 12 | 13 | /// A realm that holds isolated processes. 14 | /// 15 | /// # Destruction 16 | /// When the realm is dropped, all processes in the realm are released from the realm, but are not killed. 17 | #[derive(Debug)] 18 | pub struct Realm(sys::Realm); 19 | impl Realm { 20 | pub fn new() -> std::io::Result { 21 | sys::Realm::new().map(Self) 22 | } 23 | 24 | pub fn set_cpu_limit(&self, max: u64) -> std::io::Result<()> { 25 | self.0.set_cpu_limit(max) 26 | } 27 | 28 | pub fn set_mem_limit(&self, max: usize) -> std::io::Result<()> { 29 | self.0.set_mem_limit(max) 30 | } 31 | 32 | /// Adds a process to the realm. 33 | pub fn add(&self, pid: i64) -> std::io::Result<()> { 34 | self.0.add(pid) 35 | } 36 | 37 | /// Force-kills all processes in the realm. 38 | pub fn kill(&self) -> std::io::Result<()> { 39 | self.0.kill() 40 | } 41 | 42 | /// Returns how many bytes of memory were used in the realm. 43 | pub fn memory_usage(&self) -> std::io::Result { 44 | self.0.memory_usage() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /airupfx/airupfx-isolator/src/linux.rs: -------------------------------------------------------------------------------- 1 | use cgroups_rs::{ 2 | CgroupPid, cgroup_builder::CgroupBuilder, cpu::CpuController, memory::MemController, 3 | }; 4 | use std::{ 5 | io::ErrorKind, 6 | sync::{ 7 | OnceLock, 8 | atomic::{self, AtomicU64}, 9 | }, 10 | }; 11 | 12 | static CONTROLLER: OnceLock = OnceLock::new(); 13 | 14 | #[derive(Debug)] 15 | struct RealmController { 16 | prefix: i64, 17 | id: AtomicU64, 18 | } 19 | impl RealmController { 20 | fn allocate_id(&self) -> u64 { 21 | self.id.fetch_add(1, atomic::Ordering::SeqCst) 22 | } 23 | } 24 | 25 | fn controller() -> &'static RealmController { 26 | CONTROLLER.get_or_init(|| RealmController { 27 | prefix: airupfx_time::timestamp_ms(), 28 | id: AtomicU64::new(1), 29 | }) 30 | } 31 | 32 | #[derive(Debug)] 33 | pub struct Realm { 34 | cg: cgroups_rs::Cgroup, 35 | } 36 | impl Realm { 37 | pub fn new() -> std::io::Result { 38 | Self::pid_detect()?; 39 | let ctrl = controller(); 40 | let id = ctrl.allocate_id(); 41 | let hier = cgroups_rs::hierarchies::auto(); 42 | let cg = CgroupBuilder::new(&format!("airup_{}_{id}", ctrl.prefix)) 43 | .cpu() 44 | .done() 45 | .memory() 46 | .done() 47 | .build(hier) 48 | .map_err(|x| std::io::Error::new(ErrorKind::PermissionDenied, x.to_string()))?; 49 | 50 | Ok(Self { cg }) 51 | } 52 | 53 | pub fn set_cpu_limit(&self, max: u64) -> std::io::Result<()> { 54 | self.cg 55 | .controller_of::() 56 | .ok_or_else(|| std::io::Error::from(ErrorKind::PermissionDenied))? 57 | .set_shares(max) 58 | .map_err(|x| std::io::Error::new(ErrorKind::PermissionDenied, x.to_string()))?; 59 | 60 | Ok(()) 61 | } 62 | 63 | pub fn set_mem_limit(&self, max: usize) -> std::io::Result<()> { 64 | self.cg 65 | .controller_of::() 66 | .ok_or_else(|| std::io::Error::from(ErrorKind::PermissionDenied))? 67 | .set_limit(max as _) 68 | .map_err(|x| std::io::Error::new(ErrorKind::PermissionDenied, x.to_string()))?; 69 | 70 | Ok(()) 71 | } 72 | 73 | pub fn add(&self, pid: i64) -> std::io::Result<()> { 74 | self.cg 75 | .add_task_by_tgid(CgroupPid::from(pid as u64)) 76 | .map_err(|x| std::io::Error::new(ErrorKind::PermissionDenied, x.to_string()))?; 77 | 78 | Ok(()) 79 | } 80 | 81 | pub fn kill(&self) -> std::io::Result<()> { 82 | self.cg 83 | .kill() 84 | .map_err(|x| std::io::Error::new(ErrorKind::PermissionDenied, x.to_string()))?; 85 | 86 | Ok(()) 87 | } 88 | 89 | pub fn memory_usage(&self) -> std::io::Result { 90 | Ok(self 91 | .cg 92 | .controller_of::() 93 | .ok_or_else(|| std::io::Error::from(ErrorKind::PermissionDenied))? 94 | .memory_stat() 95 | .usage_in_bytes as usize) 96 | } 97 | 98 | fn pid_detect() -> std::io::Result<()> { 99 | match std::process::id() { 100 | 1 => Ok(()), 101 | _ => Err(ErrorKind::PermissionDenied.into()), 102 | } 103 | } 104 | } 105 | impl Drop for Realm { 106 | fn drop(&mut self) { 107 | _ = self.cg.delete(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /airupfx/airupfx-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-macros" 3 | authors = ["sisungo "] 4 | version = "0.7.1" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | proc-macro2 = "1" 16 | quote = "1" 17 | syn = "2" 18 | -------------------------------------------------------------------------------- /airupfx/airupfx-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{TokenStreamExt, quote}; 3 | use syn::{FnArg, ItemFn, ReturnType}; 4 | 5 | /// Declares a method for RPC. 6 | #[proc_macro_attribute] 7 | pub fn api(_: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { 8 | let input: ItemFn = match syn::parse2(item.clone().into()) { 9 | Ok(it) => it, 10 | Err(e) => return token_stream_with_error(item.into(), e).into(), 11 | }; 12 | 13 | if input.sig.unsafety.is_some() { 14 | return quote! { 15 | ::std::compile_error!("an API function cannot be marked `unsafe`"); 16 | } 17 | .into(); 18 | } 19 | 20 | let mut tuple_type = TokenStream::new(); 21 | tuple_type.append_separated( 22 | input.sig.inputs.iter().filter_map(|x| match x { 23 | FnArg::Receiver(_) => None, 24 | FnArg::Typed(y) => Some(y.ty.clone()), 25 | }), 26 | quote!(,), 27 | ); 28 | 29 | let mut pat_args = TokenStream::new(); 30 | pat_args.append_separated( 31 | input.sig.inputs.iter().filter_map(|x| match x { 32 | FnArg::Receiver(_) => None, 33 | FnArg::Typed(y) => Some(y.pat.clone()), 34 | }), 35 | quote!(,), 36 | ); 37 | 38 | let ident = input.sig.ident; 39 | let body = input.block; 40 | let vis = input.vis; 41 | let args = input.sig.inputs; 42 | let asyncness = input.sig.asyncness; 43 | let ret = match input.sig.output { 44 | ReturnType::Default => quote!(()), 45 | ReturnType::Type(_, ty) => quote!(#ty), 46 | }; 47 | 48 | quote! { 49 | #vis fn #ident(req: ::airup_sdk::rpc::Request) -> MethodFuture { 50 | #asyncness fn _impl(#args) -> #ret #body 51 | 52 | ::std::boxed::Box::pin(async move { 53 | let (#pat_args): (#tuple_type) = req.extract_params()?; 54 | _impl(#pat_args) 55 | .await 56 | .map(|x| { 57 | ::ciborium::Value::serialized(&x) 58 | .expect("IPC methods should return a value that can be serialized into CBOR") 59 | }) 60 | }) 61 | } 62 | } 63 | .into() 64 | } 65 | 66 | fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream { 67 | tokens.extend(error.into_compile_error()); 68 | tokens 69 | } 70 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-power" 3 | authors = ["sisungo "] 4 | version = "0.10.0" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | airupfx-process = { path = "../airupfx-process" } 13 | async-trait = "0.1" 14 | cfg-if = "1" 15 | tokio = { workspace = true } 16 | 17 | [target.'cfg(target_family = "unix")'.dependencies] 18 | libc = "0.2" 19 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::PowerManager; 2 | 3 | pub fn power_manager() -> &'static dyn PowerManager { 4 | &crate::Fallback 5 | } 6 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/src/freebsd.rs: -------------------------------------------------------------------------------- 1 | //! FreeBSD power management. 2 | 3 | use crate::PowerManager; 4 | use std::convert::Infallible; 5 | 6 | #[derive(Default)] 7 | pub struct Power; 8 | #[async_trait::async_trait] 9 | impl PowerManager for Power { 10 | async fn poweroff(&self) -> std::io::Result { 11 | crate::unix::prepare().await; 12 | reboot(libc::RB_POWEROFF) 13 | } 14 | 15 | async fn reboot(&self) -> std::io::Result { 16 | crate::unix::prepare().await; 17 | reboot(0) 18 | } 19 | 20 | async fn halt(&self) -> std::io::Result { 21 | crate::unix::prepare().await; 22 | reboot(libc::RB_HALT) 23 | } 24 | 25 | async fn userspace(&self) -> std::io::Result { 26 | crate::unix::prepare().await; 27 | airupfx_process::reload_image() 28 | } 29 | } 30 | 31 | fn reboot(cmd: libc::c_int) -> std::io::Result { 32 | let status = unsafe { libc::reboot(cmd) }; 33 | match status { 34 | -1 => Err(std::io::Error::last_os_error()), 35 | _ => unreachable!(), 36 | } 37 | } 38 | 39 | pub fn power_manager() -> &'static dyn PowerManager { 40 | &Power 41 | } 42 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # `AirupFX` Power Management 2 | 3 | #[cfg(target_family = "unix")] 4 | mod unix; 5 | 6 | cfg_if::cfg_if! { 7 | if #[cfg(target_os = "macos")] { 8 | #[path = "macos.rs"] 9 | mod sys; 10 | } else if #[cfg(target_os = "linux")] { 11 | #[path = "linux.rs"] 12 | mod sys; 13 | } else if #[cfg(target_os = "freebsd")] { 14 | #[path = "freebsd.rs"] 15 | mod sys; 16 | } else { 17 | #[path = "common.rs"] 18 | mod sys; 19 | } 20 | } 21 | 22 | use std::convert::Infallible; 23 | 24 | /// Interface of power management. 25 | /// 26 | /// Methods in this trait are considered to securely reboot the device. These methods should never return (because the host is 27 | /// down) unless the operation failed. To reboot "securely", it should do necessary work before the device is powered down, for 28 | /// example, killing processes, syncing disks, etc. 29 | #[async_trait::async_trait] 30 | pub trait PowerManager: Send + Sync { 31 | /// Immediately powers the device off. 32 | /// 33 | /// # Errors 34 | /// An `Err(_)` is returned if the underlying OS function failed. 35 | async fn poweroff(&self) -> std::io::Result; 36 | 37 | /// Immediately reboots the device. 38 | /// 39 | /// # Errors 40 | /// An `Err(_)` is returned if the underlying OS function failed. 41 | async fn reboot(&self) -> std::io::Result; 42 | 43 | /// Immediately halts the device. 44 | /// 45 | /// # Errors 46 | /// An `Err(_)` is returned if the underlying OS function failed. 47 | async fn halt(&self) -> std::io::Result; 48 | 49 | /// Performs an userspace reboot. 50 | /// 51 | /// # Errors 52 | /// An `Err(_)` is returned if it failed to perform an userspace reboot. 53 | async fn userspace(&self) -> std::io::Result; 54 | } 55 | 56 | /// A fallback implementation of `AirupFX` power management. 57 | /// 58 | /// On this implementation, when power management methods are called, it simply prints "It's now safe to turn off the device." 59 | /// to standard error stream and parks the thread if we are `pid == 1`. Otherwise, it directly exits with code `0`. 60 | #[derive(Default)] 61 | pub struct Fallback; 62 | #[async_trait::async_trait] 63 | impl PowerManager for Fallback { 64 | async fn poweroff(&self) -> std::io::Result { 65 | Self::halt_process(); 66 | } 67 | 68 | async fn reboot(&self) -> std::io::Result { 69 | Self::halt_process(); 70 | } 71 | 72 | async fn halt(&self) -> std::io::Result { 73 | Self::halt_process(); 74 | } 75 | 76 | async fn userspace(&self) -> std::io::Result { 77 | Self::halt_process(); 78 | } 79 | } 80 | impl Fallback { 81 | /// Prints "It's now safe to turn off the device." to standard error stream and parks current thread. 82 | fn halt_process() -> ! { 83 | if airupfx_process::as_pid1() { 84 | eprintln!("It's now safe to turn off the device."); 85 | loop { 86 | std::thread::park(); 87 | } 88 | } else { 89 | std::process::exit(0); 90 | } 91 | } 92 | } 93 | 94 | /// Returns a reference to the global unique [`PowerManager`] instance. 95 | /// 96 | /// If the process is `pid == 1`, the platform power manager is used, otherwise the fallback power manager [`Fallback`] is 97 | /// always returned. 98 | #[must_use] 99 | pub fn power_manager() -> &'static dyn PowerManager { 100 | if airupfx_process::as_pid1() { 101 | sys::power_manager() 102 | } else { 103 | &Fallback 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/src/linux.rs: -------------------------------------------------------------------------------- 1 | //! Linux power management. 2 | 3 | use crate::PowerManager; 4 | use libc::{ 5 | LINUX_REBOOT_CMD_HALT, LINUX_REBOOT_CMD_POWER_OFF, LINUX_REBOOT_CMD_RESTART, 6 | LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, SYS_reboot, c_void, syscall, 7 | }; 8 | use std::{convert::Infallible, ptr::NonNull}; 9 | 10 | #[derive(Default)] 11 | pub struct Power; 12 | #[async_trait::async_trait] 13 | impl PowerManager for Power { 14 | async fn poweroff(&self) -> std::io::Result { 15 | crate::unix::prepare().await; 16 | reboot(LINUX_REBOOT_CMD_POWER_OFF, None)?; 17 | unreachable!() 18 | } 19 | 20 | async fn reboot(&self) -> std::io::Result { 21 | crate::unix::prepare().await; 22 | reboot(LINUX_REBOOT_CMD_RESTART, None)?; 23 | unreachable!() 24 | } 25 | 26 | async fn halt(&self) -> std::io::Result { 27 | crate::unix::prepare().await; 28 | reboot(LINUX_REBOOT_CMD_HALT, None)?; 29 | unreachable!() 30 | } 31 | 32 | async fn userspace(&self) -> std::io::Result { 33 | crate::unix::prepare().await; 34 | airupfx_process::reload_image() 35 | } 36 | } 37 | 38 | fn reboot(cmd: libc::c_int, arg: Option>) -> std::io::Result<()> { 39 | let status = unsafe { 40 | syscall( 41 | SYS_reboot, 42 | LINUX_REBOOT_MAGIC1, 43 | LINUX_REBOOT_MAGIC2, 44 | cmd, 45 | arg, 46 | ) 47 | }; 48 | if status < 0 { 49 | Err(std::io::ErrorKind::PermissionDenied.into()) 50 | } else { 51 | Ok(()) 52 | } 53 | } 54 | 55 | pub fn power_manager() -> &'static dyn PowerManager { 56 | &Power 57 | } 58 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/src/macos.rs: -------------------------------------------------------------------------------- 1 | //! Apple macOS power management. 2 | 3 | #[link(name = "System")] 4 | unsafe extern "C" { 5 | /// Reboots the system or halts the processor. 6 | /// 7 | /// This is an Apple Private API. See `reboot(2)` for more details. 8 | #[link_name = "reboot"] 9 | safe fn sys_reboot(howto: libc::c_int) -> libc::c_int; 10 | } 11 | 12 | use crate::PowerManager; 13 | use std::convert::Infallible; 14 | 15 | const RB_AUTOBOOT: libc::c_int = 0; 16 | const RB_HALT: libc::c_int = 0x08; 17 | 18 | #[derive(Default)] 19 | pub struct Power; 20 | #[async_trait::async_trait] 21 | impl PowerManager for Power { 22 | async fn poweroff(&self) -> std::io::Result { 23 | crate::unix::prepare().await; 24 | reboot(RB_HALT) 25 | } 26 | 27 | async fn reboot(&self) -> std::io::Result { 28 | crate::unix::prepare().await; 29 | reboot(RB_AUTOBOOT) 30 | } 31 | 32 | async fn halt(&self) -> std::io::Result { 33 | crate::unix::prepare().await; 34 | reboot(RB_HALT) 35 | } 36 | 37 | async fn userspace(&self) -> std::io::Result { 38 | crate::unix::prepare().await; 39 | airupfx_process::reload_image() 40 | } 41 | } 42 | 43 | fn reboot(cmd: libc::c_int) -> std::io::Result { 44 | let status = sys_reboot(cmd); 45 | match status { 46 | -1 => Err(std::io::Error::last_os_error()), 47 | _ => unreachable!(), 48 | } 49 | } 50 | 51 | pub fn power_manager() -> &'static dyn PowerManager { 52 | &Power 53 | } 54 | -------------------------------------------------------------------------------- /airupfx/airupfx-power/src/unix.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | unused, 3 | reason = "UNIX systems depending on this module may provide their own implementation for some function" 4 | )] 5 | 6 | use std::time::Duration; 7 | 8 | const TIMEOUT: Duration = Duration::from_millis(5000); 9 | 10 | /// Prepares for system reboot, including killing all processes, syncing disks and unmounting filesystems. 11 | pub(crate) async fn prepare() { 12 | kill_all(TIMEOUT).await; 13 | sync_disks(); 14 | _ = umount_all_filesystems().await; 15 | } 16 | 17 | /// Sends a signal to all running processes, then wait for them to be terminated. If the timeout expired, the processes are 18 | /// force-killed. 19 | pub(crate) async fn kill_all(timeout: Duration) { 20 | eprintln!("Sending SIGTERM to all processes"); 21 | _ = kill_all_processes(libc::SIGTERM); 22 | 23 | eprintln!("Waiting for all processes to be terminated"); 24 | let _lock = airupfx_process::lock().await; 25 | _ = tokio::time::timeout( 26 | timeout, 27 | tokio::task::spawn_blocking(|| { 28 | let mut status = 0; 29 | while unsafe { libc::wait(&mut status) > 0 } {} 30 | }), 31 | ) 32 | .await; 33 | drop(_lock); 34 | 35 | eprintln!("Sending SIGKILL to all processes"); 36 | _ = kill_all_processes(libc::SIGKILL); 37 | } 38 | 39 | /// Flushes the block buffer cache and synchronizes disk caches. 40 | fn sync_disks() { 41 | unsafe { 42 | libc::sync(); 43 | } 44 | } 45 | 46 | /// Sends the specified signal to all processes in the system. 47 | fn kill_all_processes(signum: libc::c_int) -> std::io::Result<()> { 48 | let x = unsafe { libc::kill(-1, signum) }; 49 | match x { 50 | 0 => Ok(()), 51 | _ => Err(std::io::Error::last_os_error()), 52 | } 53 | } 54 | 55 | /// Unmounts all filesystems. 56 | async fn umount_all_filesystems() -> std::io::Result<()> { 57 | airupfx_process::Command::new("umount") 58 | .arg("-a") 59 | .spawn() 60 | .await? 61 | .wait() 62 | .await 63 | .map_err(|_| std::io::Error::from(std::io::ErrorKind::PermissionDenied))?; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /airupfx/airupfx-process/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-process" 3 | authors = ["sisungo "] 4 | version = "0.8.1" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | cfg-if = "1" 13 | airupfx-env = { path = "../airupfx-env" } 14 | airupfx-io = { path = "../airupfx-io" } 15 | libc = "0.2" 16 | thiserror = "1" 17 | tokio = { workspace = true } 18 | -------------------------------------------------------------------------------- /airupfx/airupfx-signal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-signal" 3 | authors = ["sisungo "] 4 | version = "0.6.1" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | cfg-if = "1" 13 | libc = "0.2" 14 | tokio = { workspace = true } 15 | -------------------------------------------------------------------------------- /airupfx/airupfx-signal/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Signal handling for Unix platforms. 2 | 3 | cfg_if::cfg_if! { 4 | if #[cfg(target_family = "unix")] { 5 | #[path = "unix.rs"] 6 | mod sys; 7 | } else { 8 | std::compile_error!("This target is not supported by `Airup` yet. Consider opening an issue at https://github.com/sisungo/airup/issues?"); 9 | } 10 | } 11 | 12 | pub use sys::*; 13 | 14 | /// Ignores all signals in the list. Any errors will be ignored. 15 | pub fn ignore_all>(signum_list: I) { 16 | signum_list.into_iter().for_each(|signum| { 17 | _ = ignore(signum); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /airupfx/airupfx-signal/src/unix.rs: -------------------------------------------------------------------------------- 1 | use libc::c_int; 2 | use std::future::Future; 3 | use tokio::signal::unix::SignalKind; 4 | 5 | /// Terminal line hangup. 6 | pub const SIGHUP: c_int = libc::SIGHUP; 7 | 8 | /// Interrupt program. 9 | pub const SIGINT: c_int = libc::SIGINT; 10 | 11 | /// Quit program. 12 | pub const SIGQUIT: c_int = libc::SIGQUIT; 13 | 14 | /// Write on a pipe with no reader. 15 | pub const SIGPIPE: c_int = libc::SIGPIPE; 16 | 17 | /// Software termination signal. 18 | pub const SIGTERM: c_int = libc::SIGTERM; 19 | 20 | /// Stop signal generated from keyboard. 21 | pub const SIGTSTP: c_int = libc::SIGTSTP; 22 | 23 | /// Child status has changed. 24 | pub const SIGCHLD: c_int = libc::SIGCHLD; 25 | 26 | /// Background read attempted from control terminal. 27 | pub const SIGTTIN: c_int = libc::SIGTTIN; 28 | 29 | /// Background write attempted to control terminal. 30 | pub const SIGTTOU: c_int = libc::SIGTTOU; 31 | 32 | /// I/O is possible on a descriptor (see fcntl(2)). 33 | pub const SIGIO: c_int = libc::SIGIO; 34 | 35 | /// User defined signal 1. 36 | pub const SIGUSR1: c_int = libc::SIGUSR1; 37 | 38 | /// User defined signal 2. 39 | pub const SIGUSR2: c_int = libc::SIGUSR2; 40 | 41 | /// Window size change 42 | pub const SIGWINCH: c_int = libc::SIGWINCH; 43 | 44 | /// Kills the process. 45 | pub const SIGKILL: c_int = libc::SIGKILL; 46 | 47 | /// Registers a signal handler. 48 | /// 49 | /// # Errors 50 | /// An `Err(_)` is returned if the underlying OS function failed. 51 | pub fn signal< 52 | F: FnOnce(i32) -> T + Clone + Send + Sync + 'static, 53 | T: Future + Send + 'static, 54 | >( 55 | signum: i32, 56 | op: F, 57 | ) -> std::io::Result<()> { 58 | let mut signal = tokio::signal::unix::signal(SignalKind::from_raw(signum))?; 59 | tokio::spawn(async move { 60 | loop { 61 | signal.recv().await; 62 | tokio::spawn(op.clone()(signum)); 63 | } 64 | }); 65 | 66 | Ok(()) 67 | } 68 | 69 | /// Ignores a signal. 70 | /// 71 | /// # Errors 72 | /// An `Err(_)` is returned if the underlying OS function failed. 73 | pub fn ignore(signum: i32) -> std::io::Result<()> { 74 | // Why not using `SIG_IGN`: it is by default inherited by child processes. 75 | signal(signum, |_| async {}) 76 | } 77 | 78 | /// Initializes necessary primitives. 79 | pub fn init() { 80 | if std::process::id() == 1 { 81 | for signum in [ 82 | libc::SIGSEGV, 83 | libc::SIGBUS, 84 | libc::SIGILL, 85 | libc::SIGFPE, 86 | libc::SIGABRT, 87 | libc::SIGSYS, 88 | ] { 89 | unsafe { 90 | libc::signal(signum, fatal_error_handler as *const u8 as usize); 91 | } 92 | } 93 | } 94 | } 95 | 96 | /// A signal handler that handles fatal errors, like `SIGSEGV`, `SIGABRT`, etc. 97 | /// 98 | /// This signal handler is only registered when we are `pid == 1`. It will firstly set `SIGCHLD` to `SIG_IGN`, so the kernel 99 | /// would reap child processes, then print an error message to stderr. After that, this will hang the process forever. 100 | extern "C" fn fatal_error_handler(signum: libc::c_int) { 101 | let begin = b"airupd[1]: caught "; 102 | let signal = match signum { 103 | libc::SIGSEGV => &b"SIGSEGV"[..], 104 | libc::SIGBUS => &b"SIGBUS"[..], 105 | libc::SIGILL => &b"SIGILL"[..], 106 | libc::SIGFPE => &b"SIGFPE"[..], 107 | libc::SIGABRT => &b"SIGABRT"[..], 108 | libc::SIGSYS => &b"SIGSYS"[..], 109 | _ => &b"unknown signal"[..], 110 | }; 111 | let end = b", this is a fatal error.\n"; 112 | 113 | unsafe { 114 | libc::signal(SIGCHLD, libc::SIG_IGN); 115 | libc::write(2, begin.as_ptr() as *const _, begin.len()); 116 | libc::write(2, signal.as_ptr() as *const _, signal.len()); 117 | libc::write(2, end.as_ptr() as *const _, end.len()); 118 | } 119 | 120 | loop { 121 | std::hint::spin_loop(); 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | #[tokio::test] 128 | async fn ignore() { 129 | super::ignore(super::SIGUSR1).unwrap(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /airupfx/airupfx-time/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx-time" 3 | authors = ["sisungo "] 4 | version = "0.10.6" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | tokio = { workspace = true } 13 | -------------------------------------------------------------------------------- /airupfx/airupfx-time/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for tracking time. 2 | 3 | use std::time::Duration; 4 | use tokio::time::{Instant, Interval}; 5 | 6 | /// A countdown timer. 7 | #[derive(Debug)] 8 | pub struct Countdown { 9 | inst: Instant, 10 | dur: Option, 11 | } 12 | impl Countdown { 13 | /// Creates a new [`Countdown`] instance with the given [`Duration`]. 14 | pub fn new(dur: Option) -> Self { 15 | Self { 16 | inst: Instant::now(), 17 | dur, 18 | } 19 | } 20 | 21 | /// Returns the time left until the timeout expired, returning [`Duration::ZERO`] if the timeout expired. 22 | #[inline] 23 | pub fn left(&self) -> Option { 24 | self.dur.map(|x| x.saturating_sub(self.inst.elapsed())) 25 | } 26 | } 27 | 28 | /// An alarm timer. 29 | #[derive(Debug)] 30 | pub struct Alarm { 31 | dur: Duration, 32 | interval: Option, 33 | } 34 | impl Alarm { 35 | pub fn new(dur: Duration) -> Self { 36 | Self { 37 | dur, 38 | interval: Some(tokio::time::interval(dur)), 39 | } 40 | } 41 | 42 | pub fn enable(&mut self) { 43 | self.interval = Some(tokio::time::interval(self.dur)); 44 | } 45 | 46 | pub fn disable(&mut self) { 47 | self.interval = None; 48 | } 49 | 50 | pub fn reset(&mut self) { 51 | if let Some(interval) = self.interval.as_mut() { 52 | interval.reset(); 53 | } 54 | } 55 | 56 | pub async fn wait(&mut self) -> Option<()> { 57 | match &mut self.interval { 58 | Some(x) => { 59 | x.tick().await; 60 | Some(()) 61 | } 62 | None => None, 63 | } 64 | } 65 | } 66 | 67 | /// Creates a countdown timer with given [`Duration`]. 68 | #[inline] 69 | pub fn countdown(dur: Option) -> Countdown { 70 | Countdown::new(dur) 71 | } 72 | 73 | /// Returns how many milliseconds passed since `1970-01-01 00:00:00`. 74 | pub fn timestamp_ms() -> i64 { 75 | match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { 76 | Ok(x) => x.as_millis() as _, 77 | Err(err) => -(err.duration().as_millis() as i64), 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | #[test] 84 | fn timestamp() { 85 | assert!(crate::timestamp_ms() > 0); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /airupfx/airupfx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airupfx" 3 | authors = ["sisungo "] 4 | version = "0.10.0" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | cgroups = ["airupfx-isolator/cgroups"] 13 | selinux = ["dep:selinux"] 14 | 15 | [dependencies] 16 | airupfx-env = { path = "../airupfx-env" } 17 | airupfx-fs = { path = "../airupfx-fs" } 18 | airupfx-isolator = { path = "../airupfx-isolator" } 19 | airupfx-io = { path = "../airupfx-io" } 20 | airupfx-power = { path = "../airupfx-power" } 21 | airupfx-process = { path = "../airupfx-process" } 22 | airupfx-signal = { path = "../airupfx-signal" } 23 | airupfx-time = { path = "../airupfx-time" } 24 | airupfx-macros = { path = "../airupfx-macros" } 25 | airupfx-extensions = { path = "../airupfx-extensions" } 26 | tracing = "0.1" 27 | 28 | [target.'cfg(target_os = "linux")'.dependencies] 29 | selinux = { version = "0.4", optional = true } -------------------------------------------------------------------------------- /airupfx/airupfx/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # AirupFX 2 | //! Base OS support library of Airup. This is internal to the `airup` project and is NOT subjected to be published as a part of 3 | //! the Airup SDK. 4 | //! 5 | //! Since Airup v0.5.0, AirupFX version is no longer synced with other components. 6 | 7 | pub mod prelude; 8 | pub mod util; 9 | 10 | pub use airupfx_env as env; 11 | pub use airupfx_extensions as extensions; 12 | pub use airupfx_fs as fs; 13 | pub use airupfx_io as io; 14 | pub use airupfx_isolator as isolator; 15 | pub use airupfx_macros as macros; 16 | pub use airupfx_power as power; 17 | pub use airupfx_process as process; 18 | pub use airupfx_signal as signal; 19 | pub use airupfx_time as time; 20 | 21 | pub async fn init() { 22 | #[cfg(feature = "selinux")] 23 | unsafe { 24 | // FIXME: We can't avoid use of environmental variables here, but can we at least make it look better? 25 | if process::as_pid1() && env::take_var("AIRUP_TEMP_SELINUX_INITIALIZED").is_err() { 26 | _ = selinux::policy::load_initial(); 27 | std::env::set_var("AIRUP_TEMP_SELINUX_INITIALIZED", "1"); 28 | _ = process::reload_image(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /airupfx/airupfx/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! The `AirupFX` prelude. 2 | 3 | pub use crate::power::{PowerManager, power_manager}; 4 | pub use crate::time::{Countdown, countdown, timestamp_ms}; 5 | pub use crate::util::{BoxFuture, ResultExt as _}; 6 | -------------------------------------------------------------------------------- /airupfx/airupfx/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Extension to the standard library. 2 | 3 | use std::{future::Future, pin::Pin}; 4 | 5 | pub type BoxFuture<'a, T> = Pin + Send + 'a>>; 6 | 7 | /// An extension for standard [`Result`] type to support logging. 8 | pub trait ResultExt { 9 | /// Returns the contained `Ok` value, consuming the `self` value. 10 | fn unwrap_log(self, why: &str) -> impl Future; 11 | } 12 | impl ResultExt for Result 13 | where 14 | E: std::fmt::Display, 15 | { 16 | async fn unwrap_log(self, why: &str) -> T { 17 | match self { 18 | Ok(val) => val, 19 | Err(err) => { 20 | tracing::error!(target: "console", "{}: {}", why, err); 21 | if crate::process::as_pid1() { 22 | loop { 23 | if let Err(err) = shell().await { 24 | tracing::error!(target: "console", "Failed to start `/bin/sh`: {err}"); 25 | } 26 | let Err(err) = crate::process::reload_image(); 27 | tracing::error!(target: "console", "Failed to reload `airupd` process image: {err}"); 28 | } 29 | } else { 30 | std::process::exit(1); 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | async fn shell() -> std::io::Result<()> { 38 | let cmd = crate::process::Command::new("/bin/sh").spawn().await?; 39 | cmd.wait() 40 | .await 41 | .map_err(|_| std::io::Error::from(std::io::ErrorKind::PermissionDenied))?; 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Airup Documentation 2 | Choose a language: [English \(US\)](en-US/index.md) | [简体中文](zh-CN/index.md) 3 | 4 | Not in the list? [Read the Guide](i18n_guide.md) to make a new translation! 5 | -------------------------------------------------------------------------------- /docs/artwork/LICENSE: -------------------------------------------------------------------------------- 1 | Fonts used in "airup_logo.png", "airup_logo_320x200.png" and "airup_logo_640x400.png": 2 | ``` 3 | Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | 10 | ----------------------------------------------------------- 11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 12 | ----------------------------------------------------------- 13 | 14 | PREAMBLE 15 | The goals of the Open Font License (OFL) are to stimulate worldwide 16 | development of collaborative font projects, to support the font creation 17 | efforts of academic and linguistic communities, and to provide a free and 18 | open framework in which fonts may be shared and improved in partnership 19 | with others. 20 | 21 | The OFL allows the licensed fonts to be used, studied, modified and 22 | redistributed freely as long as they are not sold by themselves. The 23 | fonts, including any derivative works, can be bundled, embedded, 24 | redistributed and/or sold with any software provided that any reserved 25 | names are not used by derivative works. The fonts and derivatives, 26 | however, cannot be released under any other type of license. The 27 | requirement for fonts to remain under this license does not apply 28 | to any document created using the fonts or their derivatives. 29 | 30 | DEFINITIONS 31 | "Font Software" refers to the set of files released by the Copyright 32 | Holder(s) under this license and clearly marked as such. This may 33 | include source files, build scripts and documentation. 34 | 35 | "Reserved Font Name" refers to any names specified as such after the 36 | copyright statement(s). 37 | 38 | "Original Version" refers to the collection of Font Software components as 39 | distributed by the Copyright Holder(s). 40 | 41 | "Modified Version" refers to any derivative made by adding to, deleting, 42 | or substituting -- in part or in whole -- any of the components of the 43 | Original Version, by changing formats or by porting the Font Software to a 44 | new environment. 45 | 46 | "Author" refers to any designer, engineer, programmer, technical 47 | writer or other person who contributed to the Font Software. 48 | 49 | PERMISSION & CONDITIONS 50 | Permission is hereby granted, free of charge, to any person obtaining 51 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 52 | redistribute, and sell modified and unmodified copies of the Font 53 | Software, subject to the following conditions: 54 | 55 | 1) Neither the Font Software nor any of its individual components, 56 | in Original or Modified Versions, may be sold by itself. 57 | 58 | 2) Original or Modified Versions of the Font Software may be bundled, 59 | redistributed and/or sold with any software, provided that each copy 60 | contains the above copyright notice and this license. These can be 61 | included either as stand-alone text files, human-readable headers or 62 | in the appropriate machine-readable metadata fields within text or 63 | binary files as long as those fields can be easily viewed by the user. 64 | 65 | 3) No Modified Version of the Font Software may use the Reserved Font 66 | Name(s) unless explicit written permission is granted by the corresponding 67 | Copyright Holder. This restriction only applies to the primary font name as 68 | presented to the users. 69 | 70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 71 | Software shall not be used to promote, endorse or advertise any 72 | Modified Version, except to acknowledge the contribution(s) of the 73 | Copyright Holder(s) and the Author(s) or with their explicit written 74 | permission. 75 | 76 | 5) The Font Software, modified or unmodified, in part or in whole, 77 | must be distributed entirely under this license, and must not be 78 | distributed under any other license. The requirement for fonts to 79 | remain under this license does not apply to any document created 80 | using the Font Software. 81 | 82 | TERMINATION 83 | This license becomes null and void if any of the above conditions are 84 | not met. 85 | 86 | DISCLAIMER 87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 95 | OTHER DEALINGS IN THE FONT SOFTWARE. 96 | ``` -------------------------------------------------------------------------------- /docs/artwork/airup_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sisungo/airup/adfc81b59b1f7273665ae010878c67d109a84a0b/docs/artwork/airup_logo.png -------------------------------------------------------------------------------- /docs/artwork/airup_logo_320x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sisungo/airup/adfc81b59b1f7273665ae010878c67d109a84a0b/docs/artwork/airup_logo_320x200.png -------------------------------------------------------------------------------- /docs/artwork/airup_logo_640x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sisungo/airup/adfc81b59b1f7273665ae010878c67d109a84a0b/docs/artwork/airup_logo_640x400.png -------------------------------------------------------------------------------- /docs/en-US/admin_manual/airs_format.md: -------------------------------------------------------------------------------- 1 | # Airup Service Manifest File Format 2 | -------------------------------------------------------------------------------- /docs/en-US/admin_manual/index.md: -------------------------------------------------------------------------------- 1 | # Airup System Administrator's Manual 2 | File formats: 3 | - [Airup Service Manifest File Format](airs_format.md) 4 | 5 | Uncategoried content: 6 | - [Tutorial: Use Airup to Build a Linux Distro](linux_distro_tutorial.md) 7 | - [Tutorial: Use Airup as Standlone Service Supervisor](standalone_supervisor.md) 8 | -------------------------------------------------------------------------------- /docs/en-US/admin_manual/linux_distro_tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Use Airup to Build a Linux Distro 2 | -------------------------------------------------------------------------------- /docs/en-US/admin_manual/standalone_supervisor.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Use Airup as Standlone Service Supervisor 2 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/c/airup_h.md: -------------------------------------------------------------------------------- 1 | # Header File: `airup.h` 2 | 3 | ## Struct: `struct airup_error` 4 | ```c 5 | #define AIRUP_EIO 16 6 | #define AIRUP_EAPI 32 7 | #define AIRUP_EBUFTOOSMALL 64 8 | 9 | struct airup_error { 10 | uint32_t code; 11 | const char *message; 12 | const void *payload; 13 | }; 14 | ``` 15 | 16 | **Description**: Representation of an error caused by call to Airup SDK functions. 17 | 18 | **Field** *`code`*: Represents to type of the error. 19 | 20 | **Field** *`message`*: UTF-8 encoded string which describes the error in plain text. 21 | 22 | **Field** *`payload`*: Payload information attached to the error. Its type depends on value of field `code`. 23 | 24 | **Macro** *`AIRUP_EIO`*: An error code, which indicates the error is caused by an operating system IO failure. 25 | 26 | **Macro** *`AIRUP_EAPI`*: An error code, which indicates the error is an API error returned from the Airupd server. When 27 | the field `code` is set to `AIRUP_EAPI`, type of field `payload` is `struct airup_api_error`. 28 | 29 | **Macro** *`AIRUP_EBUFTOOSMALL`*: An error code, which indicates the buffer provided by the caller was too small to hold the 30 | data. 31 | 32 | ## Struct: `struct airup_api_error` 33 | ```c 34 | struct airup_api_error { 35 | const char *code; 36 | const char *message; 37 | const char *json; 38 | }; 39 | ``` 40 | 41 | **Description**: Representation of an API error returned from Airupd server. 42 | 43 | **Field** *`code`*: UTF-8 encoded string which represents to the error code. 44 | 45 | **Field** *`message`*: UTF-8 encoded string which describes the error in plain text. 46 | 47 | **Field** *`json`*: Contains raw UTF-8 encoded JSON string which is received from the Airupd server. 48 | 49 | ## Function: `airup_last_error` 50 | ```c 51 | struct airup_error airup_last_error(void); 52 | ``` 53 | 54 | **Description**: Returns the error occurred by last call to an Airup SDK function. This is thread-safe, since Airup errors 55 | are in thread-local storage. 56 | 57 | ## Function: `airup_connect` 58 | ```c 59 | airup_connection *airup_connect(const char *path); 60 | ``` 61 | 62 | **Description**: Attempts to connect to IPC port on specified `path` in Airup's IPC protocol. On success, a pointer to the 63 | connection opened is returned. On failure, NULL is returned and current thread's Airup error is set. 64 | 65 | ## Function: `airup_disconnect` 66 | ```c 67 | void airup_disconnect(airup_connection *connection); 68 | ``` 69 | 70 | **Description**: Closes the connection `connection`. After calling this method `connection` is released and is no longer 71 | available. 72 | 73 | ## Function: `airup_default_path` 74 | ```c 75 | const char *airup_default_path(void); 76 | ``` 77 | 78 | **Description**:Returns default path to Airup's IPC port. If environment variable `AIRUP_SOCK` is present, its value is 79 | returned. Otherwise a value calculated from `build_manifest.json` provided at compile-time of this SDK is returned. 80 | 81 | ## Function: `airup_build_manifest` 82 | ```c 83 | const char *airup_build_manifest(void); 84 | ``` 85 | 86 | **Description**:Returns JSON string representation of the SDK's built-in build manifest, a.k.a content of compile-time 87 | `build_manifest.json` of the SDK. 88 | 89 | ## Function: `airup_start_service` 90 | ```c 91 | int airup_start_service(airup_connection *connection, const char *name); 92 | ``` 93 | 94 | **Description**: Invokes `system.start_service` method on connection `connection` with parameter `name`. On success, 95 | returns `0`. On failure, returns `-1` and current thread's Airup error is set. 96 | 97 | ## Function: `airup_stop_service` 98 | ```c 99 | int airup_stop_service(airup_connection *connection, const char *name); 100 | ``` 101 | 102 | **Description**: Invokes `system.stop_service` method on connection `connection` with parameter `name`. On success, 103 | returns `0`. On failure, returns `-1` and current thread's Airup error is set. 104 | 105 | ## Function: `airup_trigger_event` 106 | ```c 107 | int airup_trigger_event(airup_connection *connection, const char *id, const char *payload); 108 | ``` 109 | 110 | **Description**: Invokes `system.trigger_event` method on connection `connection` with an `Event` object constructed with id 111 | `id` and payload `payload`. On success, returns `0`. On failure, returns `-1` and current thread's Airup error is set. 112 | 113 | ## Function: 114 | ```c 115 | int airup_server_version(airup_connection *connection, char *buffer, size_t size); 116 | ``` 117 | 118 | **Description**: Invokes `info.version` method on connection `connection`, fill the buffer `buffer` whose size is `size` with 119 | the returned string. On success, returns `0`. On failure, returns `-1` and current thread's Airup error is set. 120 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/c/index.md: -------------------------------------------------------------------------------- 1 | # Airup SDK for C manual 2 | Airup SDK for C is an implementation of the Airup SDK. It is for C99/C11/C23 and C++. 3 | 4 | 🚧 **WARNING** 🚧: The SDK is currently being rewritten and the old SDK has been deleted from repository. Please refer to older 5 | versions (v0.10.3) to get the SDK, or wait for the newly-written C SDK is done. 6 | 7 | ## Example 8 | ```c 9 | #include 10 | #include 11 | 12 | int main(int argc, char *argv[]) { 13 | char *path = airup_default_path(); 14 | airup_connection *conn = airup_connect(path); 15 | if (conn == NULL) { 16 | printf("error: failed to connect to airup daemon: %s\n", airup_last_error().message); 17 | return 1; 18 | } 19 | if (argc > 1) { 20 | int status = airup_start_service(conn, argv[1]); 21 | if (status == -1) { 22 | printf("error: failed to start service %s: %s\n", argv[1], airup_last_error().message); 23 | airup_disconnect(conn); 24 | return 1; 25 | } 26 | } else { 27 | printf("error: no service specified to start\n"); 28 | airup_disconnect(conn); 29 | return 1; 30 | } 31 | } 32 | ``` 33 | 34 | This is a simple Airup client program which starts a service read from its first command-line argument. 35 | 36 | ## Header Files 37 | - [`airup.h`](airup_h.md) 38 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/predefined_events/index.md: -------------------------------------------------------------------------------- 1 | # Airup Pre-defined Events Manual 2 | Airup Pre-defined Events are a set of events that are pre-defined by Airup and has conventional meanings. 3 | 4 | ## List of Airup Pre-defined Events 5 | - `notify_active`: Notifies the Airup daemon that the specified daemon is now active. 6 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/rpc/debug.md: -------------------------------------------------------------------------------- 1 | # Module: `debug` 2 | 3 | Module `debug` provides methods that are used for Airup debugging. 4 | 5 | ## Method: `debug.disconnect` 6 | 7 | **Name**: `debug.disconnect` 8 | 9 | **Parameters**: None 10 | 11 | **Return Value**: Never returns 12 | 13 | **Description**: Disconnects current IPC connection. 14 | 15 | ## Method: `debug.exit` 16 | 17 | **Name**: `debug.exit` 18 | 19 | **Parameters**: None 20 | 21 | **Return Value**: `null` 22 | 23 | **Description**: Makes `airupd` daemon process exit. 24 | 25 | ## Method: `debug.echo_raw` 26 | 27 | **Name**: `debug.echo_raw` 28 | 29 | **Parameters**: `Response` object 30 | 31 | **Return Value**: Content of the parameter. 32 | 33 | **Description**: Returns the `Response` object provided by the parameter. 34 | 35 | **NOTE**: This is an API exposed as an Airup internal implementation detail and is subject to change. 36 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/rpc/index.md: -------------------------------------------------------------------------------- 1 | # Airup RPC API manual 2 | This is manual of Airup's RPC APIs. 3 | 4 | ## Modules 5 | - [debug](debug.md): Provides useful methods for debugging. 6 | - [info](info.md): Provides methods that are used to query information about Airup and the system. 7 | - [system](system.md): Provides methods that are used to manage the system. 8 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/rpc/info.md: -------------------------------------------------------------------------------- 1 | # Module: `info` 2 | 3 | Module `info` provides methods that are used for querying information about Airup and the system. 4 | 5 | ## Method: `info.version` 6 | 7 | **Name**: `info.version` 8 | 9 | **Parameters**: None 10 | 11 | **Return Value**: `string` 12 | 13 | **Description**: Queries version of `airupd`. 14 | 15 | ## Method: `info.build_manifest` 16 | 17 | **Name**: `info.build_manifest` 18 | 19 | **Parameters**: None 20 | 21 | **Return Value**: `BuildManifest` object 22 | 23 | **Description**: Returns the `BuildManifest` object which current `airupd` instance is using. 24 | -------------------------------------------------------------------------------- /docs/en-US/api_manual/rpc/system.md: -------------------------------------------------------------------------------- 1 | # Module: `system` 2 | 3 | Module `system` provides methods for managing the system. 4 | 5 | ## Object: `Event` 6 | 7 | **Name**: `Event` 8 | 9 | **Fields**: 10 | - `id`: ID of this event. 11 | - `payload`: Payload data provided by this event. 12 | 13 | ## Method: `system.refresh` 14 | 15 | **Name**: `system.refresh` 16 | 17 | **Parameters**: None 18 | 19 | **Return Value**: `[(string, error)] (array of a tuple which combines error position and error)` 20 | 21 | **Description**: Refreshes some of `airupd`'s internal status. 22 | 23 | ## Method: `system.gc` 24 | 25 | **Name**: `system.gc` 26 | 27 | **Parameters**: None 28 | 29 | **Return Value**: `null` 30 | 31 | **Description**: Releases `airupd`'s cached system resources. 32 | 33 | ## Method: `system.query_service` 34 | 35 | **Name**: `system.query_service` 36 | 37 | **Parameters**: `string (name of service to query)` (optional) 38 | 39 | **Return Value**: `QueryService` object 40 | 41 | **Description**: Returns queried information of the service. 42 | 43 | ## Method: `system.query_system` 44 | 45 | **Name**: `system.query_system` 46 | 47 | **Parameters**: None 48 | 49 | **Return Value**: `QuerySystem` object 50 | 51 | **Description**: Returns queried macro information about the whole system. 52 | 53 | ## Method: `system.list_services` 54 | 55 | **Name**: `system.list_services` 56 | 57 | **Parameters**: None 58 | 59 | **Return Value**: `string` array 60 | 61 | **Description**: Returns a list over names of all installed services on the system. 62 | 63 | ## Method: `system.start_service` 64 | 65 | **Name**: `system.start_service` 66 | 67 | **Parameters**: `string (name of service to operate)` 68 | 69 | **Return Value**: `null` 70 | 71 | **Description**: Starts the specified service. 72 | 73 | ## Method: `system.stop_service` 74 | 75 | **Name**: `system.stop_service` 76 | 77 | **Parameters**: `string (name of service to operate)` 78 | 79 | **Return Value**: `null` 80 | 81 | **Description**: Stops the specified service. 82 | 83 | ## Method: `system.cache_service` 84 | 85 | **Name**: `system.cache_service` 86 | 87 | **Parameters**: `string (name of service to operate)` 88 | 89 | **Return Value**: `null` 90 | 91 | **Description**: Caches the specified service. 92 | 93 | ## Method: `system.uncache_service` 94 | 95 | **Name**: `system.uncache_service` 96 | 97 | **Parameters**: `string (name of service to operate)` 98 | 99 | **Return Value**: `null` 100 | 101 | **Description**: Uncaches the specified service. 102 | 103 | ## Method: `system.sideload_service` 104 | 105 | **Name**: `system.sideload_service` 106 | 107 | **Parameters**: `string (name of service)` and `Service` object 108 | 109 | **Return Value**: `null` 110 | 111 | **Description**: Caches the given service in given name. 112 | 113 | ## Method: `system.kill_service` 114 | 115 | **Name**: `system.kill_service` 116 | 117 | **Parameters**: `string (name of service to operate)` 118 | 119 | **Return Value**: `null` 120 | 121 | **Description**: Forces the specified service to stop. 122 | 123 | ## Method: `system.reload_service` 124 | 125 | **Name**: `system.reload_service` 126 | 127 | **Parameters**: `string (name of service to operate)` 128 | 129 | **Return Value**: `null` 130 | 131 | **Description**: Notifies the specified service to reload. 132 | 133 | ## Method: `system.trigger_event` 134 | 135 | **Name**: `system.trigger_event` 136 | 137 | **Parameters**: `Event` object 138 | 139 | **Return Value**: `null` 140 | 141 | **Description**: Triggers the specified event. 142 | 143 | ## Method: `system.enter_milestone` 144 | 145 | **Name**: `system.enter_milestone` 146 | 147 | **Parameters**: `string` 148 | 149 | **Return Value**: `null` 150 | 151 | **Description**: Enters a milestone. 152 | 153 | ## Method: `system.set_instance_name` 154 | 155 | **Name**: `system.set_instance_name` 156 | 157 | **Parameters**: `string` 158 | 159 | **Return Value**: `null` 160 | 161 | **Description**: Sets the server's instance name. If the string parameter was empty, it restores the default instance name. 162 | 163 | ## Method: `system.unregister_extension` 164 | 165 | **Name**: `system.unregister_extension` 166 | 167 | **Parameters**: `string (name of extension)` 168 | 169 | **Return Value**: `null` 170 | 171 | **Description**: Unregisters an Airup extension. 172 | 173 | **Possible Errors**: 174 | 175 | - `NOT_FOUND`: The specified extension was not installed yet. 176 | -------------------------------------------------------------------------------- /docs/en-US/index.md: -------------------------------------------------------------------------------- 1 | # Airup Documentation 2 | Welcome to the Airup documentation! The documentation is currently a WIP. 3 | 4 | ## User Manuals 5 | The manuals are written for users of `Airup`. 6 | - [Airup System Administrator's Manual](admin_manual/index.md). 7 | 8 | ## API Manuals 9 | The manuals are written for developers using `Airup`. 10 | - [Airup RPC API Manual](api_manual/rpc/index.md): Manual which describes the Airup RPC API. 11 | - [Airup Pre-defined Events Manual](api_manual/predefined_events/index.md): Manual which describes a set of events that are 12 | pre-defined by Airup and has conventional meanings. 13 | - [Airup SDK for C Manual](api_manual/c/index.md): Manual which describes API of Airup SDK for C. 14 | - Airup Rust SDK Documentation: API documentation of the Airup SDK for Rust. It is automatically generated by `rustdoc`. 15 | 16 | ## Man Pages 17 | Man pages can be found at [Man Pages](man_pages/index.md). 18 | -------------------------------------------------------------------------------- /docs/en-US/man_pages/airup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: airup 3 | section: 1 4 | date: 21 October 2023 5 | --- 6 | 7 | # NAME 8 | airup - user interface of airup 9 | 10 | # SYNOPSIS 11 | **airup** subcommand [arguments ...] 12 | 13 | # DESCRIPTION 14 | **airup** interfaces with **airupd** to manage and inspect the state of the system. 15 | 16 | ## SUBCOMMANDS 17 | 18 | **start** Start services. 19 | 20 | **stop** Stop services. 21 | 22 | **reload** Reload services. 23 | 24 | **restart** Restart services. 25 | 26 | **query** Query system information. 27 | 28 | **self-reload** Reload **airupd** daemon itself. 29 | 30 | **reboot** Reboot, power-off or halt the system 31 | 32 | **edit** Edit Airup files 33 | 34 | **enable** Enable an unit 35 | 36 | **disable** Disable an unit 37 | 38 | **debug** Debug Airup 39 | 40 | **help** Print the help of **airup** or the help of the given subcommand(s) 41 | 42 | # SEE ALSO 43 | **airupd** 44 | -------------------------------------------------------------------------------- /docs/en-US/man_pages/index.md: -------------------------------------------------------------------------------- 1 | # Airup Man Pages 2 | Welcome to Airup Man Pages! This directory contains document source files that are commonly converted to Unix Man Page files. 3 | 4 | ## Status 5 | Currently Airup Man Pages are highly incomplete. The source files are written in Markdown, but we are discovering whether we 6 | should switch them to other formats. 7 | 8 | ## Index 9 | - [`airup(1)`](airup.md) 10 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Airup Examples 2 | -------------------------------------------------------------------------------- /docs/i18n_guide.md: -------------------------------------------------------------------------------- 1 | # Airup Documentation Internationalization Guide 2 | Welcome to contribute for the Airup documentation project! To contribute documentation for a new language: 3 | - Copy from an exitsing translation to a new folder with the name of `RFC 1766`-represented language code, e.g.: 4 | ```shell 5 | $ cp en-US jp-JP 6 | ``` 7 | - Start translating 8 | - Commit to the repository, making a pull request 9 | -------------------------------------------------------------------------------- /docs/resources/airup-fallback-logger.airs: -------------------------------------------------------------------------------- 1 | [service] 2 | display-name = "Airup Fallback Logger" 3 | description = "An Airup extension that provides a simple logger interface for fallback use." 4 | kind = "notify" 5 | 6 | [exec] 7 | start = "/usr/libexec/airup/fallback-logger" 8 | 9 | [env] 10 | stdout = "inherit" 11 | stderr = "inherit" 12 | 13 | [env.vars] 14 | AFL_LOGPATH = "/var/log/airup" 15 | -------------------------------------------------------------------------------- /docs/resources/airupd.airs: -------------------------------------------------------------------------------- 1 | # An `.airs` service definition file that describes the Airup daemon itself. 2 | 3 | [service] 4 | display-name = "Airup" 5 | description = "System init and service supervisor" 6 | kind = "oneshot" 7 | 8 | [exec] 9 | start = "noop" 10 | reload = "& airup self-reload" 11 | -------------------------------------------------------------------------------- /docs/resources/build_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "os_name": "\u001b[36;4mAirup\u001b[0m", 3 | "config_dir": "/etc/airup", 4 | "service_dir": "/etc/airup/services", 5 | "milestone_dir": "/etc/airup/milestones", 6 | "runtime_dir": "/run/airup", 7 | "env_vars": {}, 8 | "early_cmds": [] 9 | } -------------------------------------------------------------------------------- /docs/zh-CN/admin_manual/airs_format.md: -------------------------------------------------------------------------------- 1 | # Airup 服务清单文件格式 2 | -------------------------------------------------------------------------------- /docs/zh-CN/admin_manual/index.md: -------------------------------------------------------------------------------- 1 | # Airup 系统管理员手册 2 | 文件格式: 3 | - [Airup 服务清单文件格式](airs_format.md) 4 | 5 | 未分类的内容: 6 | - [教程:使用 Airup 制作 Linux 发行版](linux_distro_tutorial.md) 7 | - [教程:将 Airup 用于独立的服务管理器](standalone_supervisor.md) 8 | -------------------------------------------------------------------------------- /docs/zh-CN/admin_manual/linux_distro_tutorial.md: -------------------------------------------------------------------------------- 1 | # 教程:使用 Airup 制作 Linux 发行版 2 | -------------------------------------------------------------------------------- /docs/zh-CN/admin_manual/standalone_supervisor.md: -------------------------------------------------------------------------------- 1 | # 教程:将 Airup 用于独立的服务管理器 2 | -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/c/airup_h.md: -------------------------------------------------------------------------------- 1 | # 头文件:`airup.h` 2 | 3 | ## 结构体:`airup_error` 4 | ```c 5 | #define AIRUP_EIO 16 6 | #define AIRUP_EAPI 32 7 | #define AIRUP_EBUFTOOSMALL 64 8 | 9 | struct airup_error { 10 | uint32_t code; 11 | const char *message; 12 | const void *payload; 13 | }; 14 | ``` 15 | 16 | **描述**:表示调用 Airup SDK 函数时发生的错误。 17 | 18 | **字段** *`code`*:表示错误的类型。 19 | 20 | **字段** *`message`*:UTF-8 编码的字符串,以纯文本描述该错误的信息。 21 | 22 | **字段** *`payload`*:该错误的附加信息。其类型取决于 `code` 字段的值。 23 | 24 | **宏** *`AIRUP_EIO`*:错误代码,表示该错误由操作系统 IO 失败导致。 25 | 26 | **宏** *`AIRUP_EAPI`*:错误代码,表示该错误由从 Airupd 服务器返回的 API 错误导致。当 `code` 字段被设置为 `AIRUP_EAPI` 时,`payload` 字段的 27 | 类型将为 `struct airup_api_error`。 28 | 29 | **宏** *`AIRUP_EBUFTOOSMALL`*:错误代码,表示该错误是因为调用者传递的缓冲区太小,不够容纳输出数据导致的。 30 | 31 | ## 结构体:`airup_api_error` 32 | ```c 33 | struct airup_api_error { 34 | const char *code; 35 | const char *message; 36 | const char *json; 37 | }; 38 | ``` 39 | 40 | **描述**:表示从 Airupd 服务器返回的 API 错误。 41 | 42 | **字段** *`code`*:UTF-8 编码的字符串,表示错误代码。 43 | 44 | **字段** *`message`*:UTF-8 编码的字符串,以纯文本描述该错误的信息。 45 | 46 | **字段** *`json`*:从 Airupd 服务器接收到的原始 JSON 字符串,以 UTF-8 编码。 47 | 48 | ## 函数:`airup_last_error` 49 | ```c 50 | struct airup_error airup_last_error(void); 51 | ``` 52 | 53 | **描述**:返回上一次调用 Airup SDK 函数出错时发生的错误。该函数是线程安全的,因为 Airup 错误保存在线程本地存储中。 54 | 55 | ## 函数:`airup_connect` 56 | ```c 57 | airup_connection *airup_connect(const char *path); 58 | ``` 59 | 60 | **描述**:尝试以 Airup 的 IPC 协议连接到指定路径 `path` 上的 Airup IPC 端口。如果成功,返回指向打开的连接的指针。如果失败,返回 `NULL`,并设置 61 | 当前线程的 Airup 错误。 62 | 63 | ## 函数: `airup_disconnect` 64 | ```c 65 | void airup_disconnect(airup_connection *connection); 66 | ``` 67 | 68 | **描述**:关闭连接 `connection`。调用该方法后 `connection` 将被释放并不再可用。 69 | 70 | ## 函数:`airup_default_path` 71 | ```c 72 | const char *airup_default_path(void); 73 | ``` 74 | 75 | **描述**:获取默认的 Airup IPC 端口路径。如果设置了 `AIRUP_SOCK` 环境变量,返回该环境变量的值,否则返回根据构建该 SDK 时 76 | 的 `build_manifest.json` 中计算出的路径。 77 | 78 | ## 函数:`airup_build_manifest` 79 | ```c 80 | const char *airup_build_manifest(void); 81 | ``` 82 | 83 | **描述**:获取此 SDK 的构建清单的 JSON 字符串表示,或称为此 Airup SDK 的编译时 `build_manifest.json` 的内容。 84 | 85 | ## 函数:`airup_start_service` 86 | ```c 87 | int airup_start_service(airup_connection *connection, const char *name); 88 | ``` 89 | 90 | **描述**:在连接 `connection` 上调用 `system.start_service` 方法并传递 `name` 作为参数。如果成功,返回 `0`。如果失败,返回 `-1` 并设置当前 91 | 线程的 Airup 错误。 92 | 93 | ## 函数:`airup_stop_service` 94 | ```c 95 | int airup_stop_service(airup_connection *connection, const char *name); 96 | ``` 97 | 98 | **描述**:在连接 `connection` 上调用 `system.stop_service` 方法并传递 `name` 作为参数。如果成功,返回 `0`。如果失败,返回 `-1` 并设置当前 99 | 线程的 Airup 错误。 100 | 101 | ## 函数:`airup_trigger_event` 102 | ```c 103 | int airup_trigger_event(airup_connection *connection, const char *id, const char *payload); 104 | ``` 105 | 106 | **描述**:在连接 `connection` 上调用 `system.trigger_event` 方法并以 `id` 作为 ID,`payload` 作为数据负载构造的 `Event` 对象。如果成功, 107 | 返回 `0`。如果失败,返回 `-1` 并设置当前线程的 Airup 错误。 108 | 109 | ## 函数:`airup_server_version` 110 | ```c 111 | int airup_server_version(airup_connection *connection, char *buffer, size_t size); 112 | ``` 113 | 114 | **描述**:在连接 `connection` 上调用 `info.version` 方法,将返回的字符串填充在大小为 `size` 的缓冲区 `buffer` 中。如果成功,返回 `0`。如果失 115 | 败,返回 `-1` 并设置当前线程的 Airup 错误。 116 | -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/c/index.md: -------------------------------------------------------------------------------- 1 | # Airup SDK for C API 手册 2 | Airup SDK for C 是 Airup SDK 的一个实现,适用于 C99/C11/C23 和 C++。 3 | 4 | 🚧 **警告** 🚧: 该 SDK 正在被重写,并且旧 SDK 已从仓库删除。若要使用该 SDK,请查看旧版(v0.10.3),或等待新的 SDK 完成。 5 | 6 | ## 示例 7 | ```c 8 | #include 9 | #include 10 | 11 | int main(int argc, char *argv[]) { 12 | char *path = airup_default_path(); 13 | airup_connection *conn = airup_connect(path); 14 | if (conn == NULL) { 15 | printf("error: failed to connect to airup daemon: %s\n", airup_last_error().message); 16 | return 1; 17 | } 18 | if (argc > 1) { 19 | int status = airup_start_service(conn, argv[1]); 20 | if (status == -1) { 21 | printf("error: failed to start service %s: %s\n", argv[1], airup_last_error().message); 22 | airup_disconnect(conn); 23 | return 1; 24 | } 25 | } else { 26 | printf("error: no service specified to start\n"); 27 | airup_disconnect(conn); 28 | return 1; 29 | } 30 | } 31 | ``` 32 | 33 | 这是一个简单的 Airup 客户端程序,能够启动一个服务。被启动的服务通过第一个命令行参数指定。 34 | 35 | ## 头文件 36 | - [`airup.h`](airup_h.md) 37 | -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/predefined_events/index.md: -------------------------------------------------------------------------------- 1 | # Airup 预定义事件手册 2 | Airup 预定义事件是一组由 Airup 预定义的事件,具有约定俗成的意义。 3 | 4 | ## Airup 预定义事件列表 5 | - `notify_active`: 用于通知 Airup 守护进程其指定的服务现已处于活跃状态。 -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/rpc/debug.md: -------------------------------------------------------------------------------- 1 | # `debug` 模块 2 | 3 | `debug` 模块内提供了用于调试 Airup 的方法。 4 | 5 | ## `debug.disconnect` 方法 6 | 7 | **名称**:`debug.disconnect` 8 | 9 | **参数**:无 10 | 11 | **返回值**:永不返回 12 | 13 | **描述**:断开当前 IPC 连接。 14 | 15 | ## `debug.exit` 方法 16 | 17 | **名称**:`debug.exit` 18 | 19 | **参数**:无 20 | 21 | **返回值**:`null` 22 | 23 | **描述**:让 `airupd` 守护进程退出。 24 | 25 | ## `debug.echo_raw` 方法 26 | 27 | **名称**:`debug.echo_raw` 28 | 29 | **参数**:`Response` 对象 30 | 31 | **返回值**:参数的内容 32 | 33 | **描述**:返回参数中提供的 `Response` 对象。 34 | 35 | **注意**:这是作为 Airup 的内部实现细节而暴露的 API,可能随时发生变更。 36 | -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/rpc/index.md: -------------------------------------------------------------------------------- 1 | # Airup RPC API 手册 2 | 这是 Airup 的 RPC API 的手册。 3 | 4 | ## 模块 5 | - [debug](debug.md): 提供用于调试的实用工具。 6 | - [info](info.md): 提供用于查询有关 Airup 和系统的信息的方法。 7 | - [system](system.md): 提供用于管理系统的方法。 8 | -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/rpc/info.md: -------------------------------------------------------------------------------- 1 | # `info` 模块 2 | 3 | `info` 模块提供了用于查询有关 Airup 和系统的信息的方法。 4 | 5 | ## `info.version` 方法 6 | 7 | **名称**:`info.version` 8 | 9 | **参数**:无 10 | 11 | **返回值**:`string` 12 | 13 | **描述**:查询 `airupd` 的版本。 14 | 15 | ## `info.build_manifest` 方法 16 | 17 | **名称**:`info.build_manifest` 18 | 19 | **参数**:无 20 | 21 | **返回值**:`BuildManifest` 对象 22 | 23 | **描述**:返回当前 `airupd` 实例正在使用的 `BuildManifest` 对象。 24 | -------------------------------------------------------------------------------- /docs/zh-CN/api_manual/rpc/system.md: -------------------------------------------------------------------------------- 1 | # `system` 模块 2 | 3 | `system` 模块提供了用于管理系统的方法。 4 | 5 | ## `Event` 对象 6 | 7 | **名称**:`Event` 8 | 9 | **字段**: 10 | - `id [string]`:该事件的 ID。 11 | - `payload [string]`:该事件的负载数据。 12 | 13 | ## `system.refresh` 方法 14 | 15 | **名称**:`system.refresh` 16 | 17 | **参数**:无 18 | 19 | **返回值**:`[(字符串, 错误)](捆绑发生错误的位置和发生的错误的元组的数组)` 20 | 21 | **描述**:刷新 `airupd` 的一些内部状态。 22 | 23 | ## `system.gc` 方法 24 | 25 | **名称**:`system.gc` 26 | 27 | **参数**:无 28 | 29 | **返回值**:`null` 30 | 31 | **描述**:释放 `airupd` 缓存的系统资源。 32 | 33 | ## `system.query_service` 方法 34 | 35 | **名称**:`system.query_service` 36 | 37 | **参数**:`字符串(要查询的服务名称)` 38 | 39 | **返回值**:`QueryService` 对象 40 | 41 | **描述**:返回查询到的有关该服务的信息。 42 | 43 | ## `system.query_system` 方法 44 | 45 | **名称**:`system.query_system` 46 | 47 | **参数**:无 48 | 49 | **返回值**:`QuerySystem` 对象 50 | 51 | **描述**:返回查询到的关于整个系统的宏观信息。 52 | 53 | ## `system.list_services` 方法 54 | 55 | **名称**:`system.list_services` 56 | 57 | **参数**:无 58 | 59 | **返回值**:`string` 数组 60 | 61 | **描述**:返回系统中已安装的所有服务的名称的列表。 62 | 63 | ## `system.start_service` 方法 64 | 65 | **名称**:`system.start_service` 66 | 67 | **参数**:`字符串(要操作的服务名称)` 68 | 69 | **返回值**:`null` 70 | 71 | **描述**:启动指定的服务。 72 | 73 | ## `system.stop_service` 方法 74 | 75 | **名称**:`system.stop_service` 76 | 77 | **参数**:`字符串(要操作的服务名称)` 78 | 79 | **返回值**:`null` 80 | 81 | **描述**:停止指定的服务。 82 | 83 | ## `system.cache_service` 方法 84 | 85 | **名称**:`system.cache_service` 86 | 87 | **参数**:`字符串(要操作的服务名称)` 88 | 89 | **返回值**:`null` 90 | 91 | **描述**:缓存指定的服务。 92 | 93 | ## `system.uncache_service` 方法 94 | 95 | **名称**:`system.uncache_service` 96 | 97 | **参数**:`字符串(要操作的服务名称)` 98 | 99 | **返回值**:`null` 100 | 101 | **描述**:取消缓存指定的服务。 102 | 103 | ## `system.sideload_service` 方法 104 | 105 | **名称**:`system.sideload_service` 106 | 107 | **参数**:`字符串(服务名称)`, `Service` 对象, `bool` 108 | 109 | **返回值**:`null` 110 | 111 | **描述**:以指定名称缓存给出的服务。 112 | 113 | ## `system.kill_service` 方法 114 | 115 | **名称**:`system.kill_service` 116 | 117 | **参数**:`字符串(要操作的服务名称)` 118 | 119 | **返回值**:`null` 120 | 121 | **描述**:强制停止指定的服务。 122 | 123 | ## `system.reload_service` 方法 124 | 125 | **名称**:`system.reload_service` 126 | 127 | **参数**:`字符串(要操作的服务名称)` 128 | 129 | **返回值**:`null` 130 | 131 | **描述**:通知指定的服务重新加载。 132 | 133 | ## `system.trigger_event` 方法 134 | 135 | **名称**:`system.trigger_event` 136 | 137 | **参数**:`Event` 对象 138 | 139 | **返回值**:`null` 140 | 141 | **描述**:触发指定事件。 142 | 143 | ## `system.set_instance_name` 方法 144 | 145 | **名称**:`system.set_instance_name` 146 | 147 | **参数**:`字符串` 148 | 149 | **返回值**:`null` 150 | 151 | **描述**:设置服务器的实例名称。如果字符串参数为空字符串,则恢复默认实例名。 152 | 153 | ## `system.enter_milestone` 方法 154 | 155 | **名称**:`system.enter_milestone` 156 | 157 | **参数**:`字符串` 158 | 159 | **返回值**:`null` 160 | 161 | **描述**:进入一个里程碑。 162 | 163 | ## `system.unregister_extension` 方法 164 | 165 | **名称**:`system.unregister_extension` 166 | 167 | **参数**:`字符串 (扩展名称)` 168 | 169 | **返回值**:`null` 170 | 171 | **描述**:取消注册一个 Airup 扩展。 172 | 173 | **可能的错误**: 174 | 175 | `NOT_FOUND`:指定的扩展还未被安装。 176 | -------------------------------------------------------------------------------- /docs/zh-CN/index.md: -------------------------------------------------------------------------------- 1 | # Airup 文档 2 | 欢迎来到 Airup 文档!该文档目前处于不完善状态。 3 | 4 | ## 用户手册 5 | 这些手册为 `Airup` 的用户准备。 6 | - [Airup 系统管理员手册](admin_manual/index.md). 7 | 8 | ## API 手册 9 | 这些手册为使用 `Airup` 的开发者准备。 10 | - [Airup RPC API 手册](api_manual/rpc/index.md):关于 Airup 的 RPC API 的手册。 11 | - [Airup 预定义事件手册](api_manual/predefined_events/index.md): 关于一组由 Airup 预定义并有着常规性意义的 Airup 事件的手册。 12 | - [Airup SDK for C 手册](api_manual/c/index.md): 关于适用于 C 语言的 Airup SDK 的手册。 13 | - Airup Rust SDK 文档:适用于 Rust 语言的 Airup SDK API 文档。由 `rustdoc` 自动生成。 14 | 15 | ## 手册页 16 | 手册页可以在 [手册页](man_pages/index.md) 找到。 17 | -------------------------------------------------------------------------------- /docs/zh-CN/man_pages/airup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: airup 3 | section: 1 4 | date: 2023-10-21 5 | --- 6 | 7 | # 名称 8 | airup - Airup的用户接口 9 | 10 | # 大纲 11 | **airup** 子命令 [参数 ...] 12 | 13 | # 描述 14 | **airup** 和 **airupd** 交互以管理和探视系统状态。 15 | 16 | ## 子命令 17 | 18 | **start** 启动服务。 19 | 20 | **stop** 停止服务。 21 | 22 | **reload** 重载服务。 23 | 24 | **restart** 重启服务。 25 | 26 | **query** 查询系统信息。 27 | 28 | **self-reload** 重载 **airupd** 服务自身。 29 | 30 | **reboot** 重启、关闭或挂起系统。 31 | 32 | **edit** 编辑 Airup 文件。 33 | 34 | **enable** 启用一个单元。 35 | 36 | **disable** 禁用一个单元。 37 | 38 | **debug** 调试 Airup。 39 | 40 | **help** 打印 **airup** 或指定子命令的帮助信息。 41 | 42 | # 参阅 43 | **airupd** 44 | -------------------------------------------------------------------------------- /docs/zh-CN/man_pages/index.md: -------------------------------------------------------------------------------- 1 | # Airup 手册页 2 | 欢迎来到 Airup 手册页!该目录包含了通常被转换为 Unix 手册页文件的源文档文件。 3 | 4 | ## 状态 5 | 目前 Airup 手册页处于很不完善的状态。源文档文件是用 Markdown 编写的,但是我们正在探索是否需要切换到其他格式。 6 | 7 | ## 索引 8 | - [`airup(1)`](airup.md) 9 | -------------------------------------------------------------------------------- /extensions/fallback-logger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fallback-logger" 3 | authors = ["sisungo "] 4 | version = "0.10.2" 5 | edition = "2024" 6 | license = "MIT" 7 | publish = false 8 | 9 | [dependencies] 10 | airupfx = { path = "../../airupfx/airupfx" } 11 | airup-sdk = { path = "../../airup-sdk", features = ["_internal"] } 12 | anyhow = "1" 13 | ciborium = "0.2" 14 | rev_lines = "0.3" 15 | serde_json = "1" 16 | tokio = { workspace = true } 17 | -------------------------------------------------------------------------------- /extensions/fallback-logger/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A simple logger for fallback use. 2 | //! 3 | //! This has some limitations and has poor performance. Being designed as a "fallback choice", the implementation aims to be 4 | //! small and simple. 5 | 6 | use airup_sdk::{Error, blocking::fs::DirChain, system::LogRecord}; 7 | use airupfx::extensions::*; 8 | use rev_lines::RevLines; 9 | use std::{io::Write, path::PathBuf, sync::OnceLock}; 10 | 11 | #[tokio::main(flavor = "current_thread")] 12 | async fn main() -> anyhow::Result<()> { 13 | Server::new("logger") 14 | .await? 15 | .mount("append", append) 16 | .mount("tail", tail) 17 | .run() 18 | .await 19 | } 20 | 21 | #[airupfx::macros::api] 22 | async fn append(subject: String, module: String, msg: Vec) -> Result<(), Error> { 23 | let mut appender = open_subject_append(&subject).map_err(|x| Error::Io { 24 | message: x.to_string(), 25 | })?; 26 | let timestamp = airupfx::time::timestamp_ms(); 27 | let mut evaluted_bytes = 0; 28 | 29 | for line in msg.split(|x| b"\n\r".contains(x)) { 30 | evaluted_bytes += line.len() + 1; 31 | if evaluted_bytes >= msg.len() && line.is_empty() { 32 | break; 33 | } 34 | 35 | let record = LogRecord { 36 | timestamp, 37 | module: module.to_owned(), 38 | message: String::from_utf8_lossy(line).into_owned(), 39 | }; 40 | writeln!( 41 | appender, 42 | "{}", 43 | serde_json::to_string(&record).unwrap().as_str() 44 | ) 45 | .map_err(|x| airup_sdk::Error::Io { 46 | message: x.to_string(), 47 | })?; 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | #[airupfx::macros::api] 54 | async fn tail(subject: String, n: usize) -> Result, Error> { 55 | if n > 1536 { 56 | return Err(Error::TimedOut); 57 | } 58 | 59 | let reader = open_subject_read(&subject).map_err(|x| Error::Io { 60 | message: x.to_string(), 61 | })?; 62 | 63 | let mut result = Vec::with_capacity(n); 64 | for line in RevLines::new(reader).take(n) { 65 | result.push( 66 | serde_json::from_str(&line.map_err(|x| Error::Io { 67 | message: x.to_string(), 68 | })?) 69 | .map_err(|x| Error::Io { 70 | message: x.to_string(), 71 | })?, 72 | ); 73 | } 74 | result.reverse(); 75 | Ok(result) 76 | } 77 | 78 | fn dir_chain_logs() -> DirChain<'static> { 79 | static PATH: OnceLock = OnceLock::new(); 80 | 81 | DirChain::new(PATH.get_or_init(|| { 82 | let Ok(path) = std::env::var("AFL_LOGPATH") else { 83 | eprintln!("airup-fallback-logger: error: environment `AFL_LOGPATH` was not set."); 84 | std::process::exit(1); 85 | }; 86 | path.into() 87 | })) 88 | } 89 | 90 | fn open_subject_append(subject: &str) -> std::io::Result { 91 | let path = dir_chain_logs().find_or_create(format!("{subject}.fallback_logger.json"))?; 92 | 93 | std::fs::File::options() 94 | .append(true) 95 | .create(true) 96 | .open(path) 97 | } 98 | 99 | fn open_subject_read(subject: &str) -> std::io::Result { 100 | let path = dir_chain_logs() 101 | .find(format!("{subject}.fallback_logger.json")) 102 | .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?; 103 | 104 | std::fs::File::open(path) 105 | } 106 | --------------------------------------------------------------------------------