commit 53d9686cfa45d9f19d14a4f86ed7922b92b40098
Author: Sylvia Ivory <git@sivory.net>
Date: Sun, 7 Jan 2024 19:23:42 -0800
Initial Commit
Diffstat:
95 files changed, 7511 insertions(+), 0 deletions(-)
diff --git a/.cargo/config.toml b/.cargo/config.toml
@@ -0,0 +1,2 @@
+[build]
+target = "x86_64-unknown-linux-musl"
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,8 @@
+/target
+/rootfs
+/.idea
+/.home
+/notes.md
+/initramfs
+/vmlinuz-virt
+*.qcow2
diff --git a/.vscode/settings.json b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "cSpell.words": [
+ "kanit",
+ "poweroff"
+ ]
+}
diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml
@@ -0,0 +1,28 @@
+when:
+ branch: master
+ event: [push, pull_request]
+
+steps:
+ - name: lint
+ image: "docker.io/rust:1.89-alpine3.22"
+ commands:
+ - apk add --no-cache musl-dev
+ - rustup component add clippy rustfmt
+ - cargo clippy -- --no-deps -Dwarnings
+ - cargo clippy --features testing -- --no-deps -Dwarnings
+ - cargo clippy --no-default-features --features timings,baked-rc,cli -- --no-deps -Dwarnings
+ - cargo fmt -- --check
+ - name: test
+ image: "docker.io/rust:1.89-alpine3.22"
+ depends_on:
+ - lint
+ commands:
+ - apk add --no-cache musl-dev just curl qemu-system-x86_64 qemu-img git bash
+ - curl -o tapview "https://gitlab.com/esr/tapview/-/raw/master/tapview"
+ - chmod +x ./tapview
+ - curl -o /usr/local/bin/lexpect "https://binaries.gayest.systems/lexpect/lexpect-lua54/target/x86_64-unknown-linux-musl/release/lexpect"
+ - chmod +x /usr/local/bin/lexpect
+ - sed 's/security_model=mapped/security_model=passthrough/' scripts/controller scripts/start -i
+ - ./scripts/prepare-vm
+ - just test > test.log
+ - cat test.log | ./tapview
diff --git a/Cargo.lock b/Cargo.lock
@@ -0,0 +1,745 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "async-channel"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
+dependencies = [
+ "async-lock",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964"
+dependencies = [
+ "async-lock",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "tracing",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+
+[[package]]
+name = "kanit-cli"
+version = "0.1.0"
+dependencies = [
+ "kanit-common",
+ "kanit-unit",
+ "nix",
+ "xflags",
+]
+
+[[package]]
+name = "kanit-common"
+version = "0.1.0"
+
+[[package]]
+name = "kanit-diagnostics"
+version = "0.1.0"
+dependencies = [
+ "kanit-common",
+ "log",
+ "send_wrapper",
+]
+
+[[package]]
+name = "kanit-executor"
+version = "0.1.0"
+dependencies = [
+ "async-executor",
+ "async-io",
+ "futures-lite",
+ "send_wrapper",
+]
+
+[[package]]
+name = "kanit-init"
+version = "0.1.0"
+dependencies = [
+ "async-fs",
+ "async-signal",
+ "futures-lite",
+ "kanit-common",
+ "kanit-diagnostics",
+ "kanit-executor",
+ "kanit-rc",
+ "libc",
+ "log",
+ "nix",
+]
+
+[[package]]
+name = "kanit-multicall"
+version = "0.1.0"
+dependencies = [
+ "kanit-cli",
+ "kanit-init",
+ "kanit-supervisor",
+]
+
+[[package]]
+name = "kanit-rc"
+version = "0.1.0"
+dependencies = [
+ "async-lock",
+ "async-process",
+ "async-trait",
+ "kanit-common",
+ "kanit-diagnostics",
+ "kanit-executor",
+ "kanit-supervisor",
+ "kanit-unit",
+ "kanit-units",
+ "log",
+ "nix",
+ "send_wrapper",
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "kanit-supervisor"
+version = "0.1.0"
+dependencies = [
+ "async-process",
+ "kanit-common",
+ "nix",
+ "xflags",
+]
+
+[[package]]
+name = "kanit-unit"
+version = "0.1.0"
+dependencies = [
+ "async-fs",
+ "async-trait",
+ "base64",
+ "blocking",
+ "kanit-common",
+ "kanit-supervisor",
+ "log",
+ "nix",
+ "nom",
+ "send_wrapper",
+]
+
+[[package]]
+name = "kanit-units"
+version = "0.1.0"
+dependencies = [
+ "async-fs",
+ "async-process",
+ "async-trait",
+ "blocking",
+ "fastrand",
+ "futures-lite",
+ "kanit-common",
+ "kanit-executor",
+ "kanit-supervisor",
+ "kanit-unit",
+ "libc",
+ "log",
+ "nix",
+ "walkdir",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "piper"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "polling"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "xflags"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d9e15fbb3de55454b0106e314b28e671279009b363e6f1d8e39fdc3bf048944"
+dependencies = [
+ "xflags-macros",
+]
+
+[[package]]
+name = "xflags-macros"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "672423d4fea7ffa2f6c25ba60031ea13dc6258070556f125cc4d790007d4a155"
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,52 @@
+[workspace]
+resolver = "2"
+members = [
+ "crates/init",
+ "crates/cli",
+ "crates/rc",
+ "crates/common",
+ "crates/supervisor",
+ "crates/executor",
+ "crates/diagnostics",
+ "crates/units",
+ "crates/unit"
+]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2024"
+
+[profile.release]
+lto = true
+codegen-units = 1
+panic = "abort" # we just forever loop anyway
+strip = "debuginfo"
+
+[profile.min]
+inherits = "release"
+strip = true
+opt-level = "z"
+
+[package]
+name = "kanit-multicall"
+version.workspace = true
+edition.workspace = true
+
+[features]
+default = ["timings", "baked-rc", "cli"]
+cli = ["dep:kanit-cli"]
+timings = ["kanit-init/timings", "kanit-cli?/blame"]
+baked-rc = ["kanit-init/baked-rc"]
+testing = ["kanit-init/testing"]
+
+[dependencies.kanit-cli]
+path = "./crates/cli"
+optional = true
+
+[dependencies.kanit-init]
+path = "./crates/init"
+
+[dependencies.kanit-supervisor]
+path = "./crates/supervisor"
+default-features = false
+features = ["cli"]
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Sylvia Ivory
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "kanit-cli"
+version.workspace = true
+edition.workspace = true
+
+[features]
+blame = []
+
+[dependencies.xflags]
+version = "0.3"
+
+[dependencies.nix]
+version = "0.29"
+features = ["user", "reboot"]
+
+[dependencies.kanit-unit]
+path = "../unit"
+
+[dependencies.kanit-common]
+path = "../common"
diff --git a/crates/cli/src/blame.rs b/crates/cli/src/blame.rs
@@ -0,0 +1,85 @@
+use std::fs;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result};
+
+use crate::flags::Blame;
+
+#[derive(Copy, Clone)]
+pub struct BlameEntry<'a> {
+ name: &'a str,
+ duration: u128,
+ level: usize,
+}
+
+impl<'a> BlameEntry<'a> {
+ pub fn parse_single_entry(line: &'a str) -> Result<Self> {
+ let mut parts = line.split_whitespace();
+
+ let name = parts.next().context("expected `name`")?;
+
+ let duration = parts
+ .next()
+ .context("expected `duration`")?
+ .parse::<u128>()
+ .context("failed to parse `duration`")?;
+
+ let level = parts
+ .next()
+ .context("expected `level`")?
+ .parse::<usize>()
+ .context("failed to parse `level`")?;
+
+ Ok(Self {
+ name,
+ duration,
+ level,
+ })
+ }
+}
+
+fn parse_blame(lines: &str) -> Vec<BlameEntry> {
+ lines
+ .lines()
+ .filter_map(|line| BlameEntry::parse_single_entry(line).ok())
+ .collect()
+}
+
+pub fn blame(opts: Blame) -> Result<()> {
+ let timings = fs::read_to_string(constants::KAN_TIMINGS).context("failed to read timings")?;
+
+ let blames = parse_blame(&timings);
+
+ let mut filtered_timings = blames
+ .iter()
+ .filter(|l| l.name.starts_with("unit:"))
+ .map(|l| BlameEntry {
+ name: l.name,
+ level: l.level,
+ duration: ((l.duration as f64) / 1000.0).round() as u128,
+ })
+ .collect::<Vec<_>>();
+
+ if opts.sorted {
+ filtered_timings.sort_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
+ }
+
+ let max_len = filtered_timings
+ .iter()
+ .map(|l| l.duration.to_string())
+ .max_by(|a, b| a.len().cmp(&b.len()))
+ .unwrap()
+ .len();
+
+ for timing in filtered_timings {
+ let dur = timing.duration.to_string();
+
+ println!(
+ "{}{dur}ms {}",
+ " ".repeat(max_len - dur.len()),
+ &timing.name[5..]
+ )
+ }
+
+ Ok(())
+}
diff --git a/crates/cli/src/flags.rs b/crates/cli/src/flags.rs
@@ -0,0 +1,51 @@
+xflags::xflags! {
+ cmd kanit {
+ /// Teardown and power-off the system.
+ cmd poweroff {
+ /// Force a power-off, not performing a teardown.
+ optional -f, --force
+ }
+ /// Teardown and reboot the system.
+ cmd reboot {
+ /// Force a reboot, not performing a teardown.
+ optional -f, --force
+ }
+ /// Teardown and halt the system.
+ cmd halt {
+ /// Force a halt, not performing a teardown.
+ optional -f, --force
+ }
+ /// Teardown and reboot the system via kexec.
+ cmd kexec {
+ /// Force a reboot via kexec, not performing a teardown.
+ optional -f, --force
+ }
+ /// Print unit startup times.
+ cmd blame {
+ // Print units sorted by startup time.
+ optional -s, --sorted
+ }
+ /// Service related utilities.
+ cmd service {
+ /// Enable a unit at the specified runlevel.
+ cmd enable {
+ /// The name of the unit.
+ required unit: String
+ /// The runlevel to enable the service at.
+ optional runlevel: String
+ }
+ /// Disable a unit at the specified runlevel.
+ cmd disable {
+ /// The name of the unit.
+ required unit: String
+ /// The runlevel to enable the service at.
+ optional runlevel: String
+ }
+ /// List all enabled units.
+ cmd list {
+ /// Shows the individual unit groups.
+ optional -p, --plan
+ }
+ }
+ }
+}
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
@@ -0,0 +1,52 @@
+use std::{env, ffi::OsString, process::ExitCode};
+
+use flags::{Kanit, KanitCmd, ServiceCmd};
+
+#[cfg(feature = "blame")]
+mod blame;
+mod flags;
+mod service;
+mod teardown;
+
+fn handle_cli_inner(args: Vec<OsString>) -> ExitCode {
+ let res = match Kanit::from_vec(args) {
+ Ok(app) => match app.subcommand {
+ KanitCmd::Poweroff(opts) => teardown::teardown("poweroff", opts.force),
+ KanitCmd::Reboot(opts) => teardown::teardown("reboot", opts.force),
+ KanitCmd::Halt(opts) => teardown::teardown("halt", opts.force),
+ KanitCmd::Kexec(opts) => teardown::teardown("kexec", opts.force),
+ #[cfg(feature = "blame")]
+ KanitCmd::Blame(opts) => blame::blame(opts),
+ #[cfg(not(feature = "blame"))]
+ KanitCmd::Blame(_) => {
+ eprintln!("kanit compiled without blame");
+ return ExitCode::FAILURE;
+ }
+ KanitCmd::Service(svc) => match svc.subcommand {
+ ServiceCmd::Enable(opts) => service::enable(opts),
+ ServiceCmd::Disable(opts) => service::disable(opts),
+ ServiceCmd::List(opts) => service::list(opts),
+ },
+ },
+ Err(e) => {
+ eprintln!("{e}");
+ return ExitCode::FAILURE;
+ }
+ };
+
+ if let Err(e) = res {
+ eprintln!("{e}");
+ return ExitCode::FAILURE;
+ }
+
+ ExitCode::SUCCESS
+}
+
+pub fn handle_cli(pop_first: bool) -> ExitCode {
+ let mut args = env::args_os();
+ if pop_first {
+ args.next();
+ }
+
+ handle_cli_inner(args.collect())
+}
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+ kanit_cli::handle_cli(true);
+}
diff --git a/crates/cli/src/service/disable.rs b/crates/cli/src/service/disable.rs
@@ -0,0 +1,19 @@
+use std::fs::remove_file;
+use std::path::PathBuf;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result};
+
+use crate::flags::Disable;
+
+pub fn disable(opts: Disable) -> Result<()> {
+ let unit = PathBuf::from(constants::KAN_ENABLED_DIR)
+ .join(opts.runlevel.unwrap_or_else(|| String::from("default")))
+ .join(opts.unit);
+
+ remove_file(&unit).context("failed to disable unit")?;
+
+ println!("delete {}", unit.to_string_lossy());
+
+ Ok(())
+}
diff --git a/crates/cli/src/service/enable.rs b/crates/cli/src/service/enable.rs
@@ -0,0 +1,24 @@
+use std::os::unix::fs::symlink;
+use std::path::PathBuf;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result};
+
+use crate::flags::Enable;
+
+pub fn enable(opts: Enable) -> Result<()> {
+ let src = PathBuf::from(constants::KAN_UNIT_DIR).join(&opts.unit);
+ let dst = PathBuf::from(constants::KAN_ENABLED_DIR)
+ .join(opts.runlevel.unwrap_or_else(|| String::from("default")))
+ .join(&opts.unit);
+
+ symlink(&src, &dst).context("failed to enable unit")?;
+
+ println!(
+ "link {} -> {}",
+ src.to_string_lossy(),
+ dst.to_string_lossy()
+ );
+
+ Ok(())
+}
diff --git a/crates/cli/src/service/list.rs b/crates/cli/src/service/list.rs
@@ -0,0 +1,42 @@
+use std::fs;
+use std::path::Path;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result, StaticError};
+use kanit_unit::formats::DependencyGrouping;
+
+use crate::flags::List;
+
+pub fn list(opts: List) -> Result<()> {
+ let group_path = Path::new(constants::KAN_DEPENDENCY_MAP);
+
+ if !group_path.exists() {
+ Err(StaticError("failed to find dependency map"))?;
+ }
+
+ let group = fs::read_to_string(group_path)
+ .context("failed to read dependency map")?
+ .parse::<DependencyGrouping>()
+ .context("failed to parse dependency map")?
+ .groups;
+
+ for (i, level) in group.iter() {
+ println!("level {i}");
+
+ if opts.plan {
+ for (o, group) in level.iter().enumerate() {
+ println!("|> group {o}");
+
+ for unit in group.iter() {
+ println!(" |> {unit}");
+ }
+ }
+ } else {
+ for unit in level.iter().flatten() {
+ println!("|> {unit}");
+ }
+ }
+ }
+
+ Ok(())
+}
diff --git a/crates/cli/src/service/mod.rs b/crates/cli/src/service/mod.rs
@@ -0,0 +1,7 @@
+pub use disable::disable;
+pub use enable::enable;
+pub use list::list;
+
+mod disable;
+mod enable;
+mod list;
diff --git a/crates/cli/src/teardown.rs b/crates/cli/src/teardown.rs
@@ -0,0 +1,28 @@
+use std::fs::write;
+
+use nix::sys::reboot::{RebootMode, reboot};
+use nix::unistd::getuid;
+
+use kanit_common::constants::KAN_PIPE;
+use kanit_common::error::{Context, Result, StaticError};
+
+pub fn teardown(cmd: &str, force: bool) -> Result<()> {
+ if !getuid().is_root() {
+ Err(StaticError("operation not permitted"))?;
+ }
+
+ if force {
+ match cmd {
+ "poweroff" => reboot(RebootMode::RB_POWER_OFF),
+ "reboot" => reboot(RebootMode::RB_AUTOBOOT),
+ "halt" => reboot(RebootMode::RB_HALT_SYSTEM),
+ "kexec" => reboot(RebootMode::RB_KEXEC),
+ _ => unreachable!(),
+ }
+ .context("Failed to reboot")?;
+ }
+
+ write(KAN_PIPE, cmd).context("failed to write to pipe")?;
+
+ Ok(())
+}
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml
@@ -0,0 +1,4 @@
+[package]
+name = "kanit-common"
+version.workspace = true
+edition.workspace = true
diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs
@@ -0,0 +1,9 @@
+pub const KAN_PIPE: &str = "/run/kan.pipe";
+pub const KAN_TIMINGS: &str = "/run/kan.timing";
+pub const KAN_PIDS: &str = "/run/kan.pid";
+pub const KAN_PATH: &str = "/bin:/sbin:/usr/bin:/usr/sbin";
+pub const KAN_SEED: &str = "/var/lib/seed";
+pub const KAN_DEPENDENCY_MAP: &str = "/var/lib/kan.map";
+pub const KAN_UNIT_DIR: &str = "/etc/kanit/system";
+pub const KAN_ENABLED_DIR: &str = "/etc/kanit/enabled";
+pub const KAN_VERSION: &str = "0.1.0";
diff --git a/crates/common/src/error.rs b/crates/common/src/error.rs
@@ -0,0 +1,151 @@
+use std::fmt::{self, Debug, Display, Formatter};
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+#[derive(Debug)]
+pub enum ErrorKind {
+ Recoverable,
+ Unrecoverable,
+}
+
+type LazyContext = dyn Send + Fn() -> String;
+
+pub struct Error {
+ kind: ErrorKind,
+ context: Option<Box<LazyContext>>,
+ original: Box<dyn std::error::Error + Send>,
+}
+
+impl Error {
+ fn new<S: Display + Send + 'static>(
+ kind: ErrorKind,
+ context: Option<S>,
+ original: Box<dyn std::error::Error + Send>,
+ ) -> Self {
+ Self {
+ context: context.map(|n| Box::new(move || n.to_string()) as Box<LazyContext>),
+ kind,
+ original,
+ }
+ }
+
+ pub fn is_recoverable(&self) -> bool {
+ matches!(self.kind, ErrorKind::Recoverable)
+ }
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ if let Some(ref context) = self.context {
+ write!(f, "{}: {}", context(), self.original)
+ } else {
+ write!(f, "{}", self.original)
+ }
+ }
+}
+
+impl Debug for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Error")
+ .field("kind", &self.kind)
+ .field("original", &self.original)
+ .finish()
+ }
+}
+
+impl<E: std::error::Error + Send + 'static> From<E> for Error {
+ fn from(error: E) -> Self {
+ Self::new::<&str>(ErrorKind::Unrecoverable, None, Box::new(error))
+ }
+}
+
+#[derive(Debug)]
+pub struct StaticError(pub &'static str);
+
+impl Display for StaticError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl std::error::Error for StaticError {}
+
+pub struct WithError(Box<LazyContext>);
+
+impl WithError {
+ pub fn with<F: Send + Fn() -> String + 'static>(e: F) -> Self {
+ Self(Box::new(e) as Box<LazyContext>)
+ }
+}
+
+impl Display for WithError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0())
+ }
+}
+
+impl Debug for WithError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Error").finish()
+ }
+}
+
+impl std::error::Error for WithError {}
+
+pub trait Context<T>: Sized {
+ fn context_kind<C: Display + Send + 'static>(self, context: C, kind: ErrorKind) -> Result<T>;
+ fn with_context_kind<C: Send + Fn() -> String + 'static>(
+ self,
+ context: C,
+ kind: ErrorKind,
+ ) -> Result<T> {
+ self.context_kind(WithError::with(context), kind)
+ }
+ fn context<C: Display + Send + 'static>(self, context: C) -> Result<T> {
+ self.context_kind(context, ErrorKind::Unrecoverable)
+ }
+ fn with_context<C: Send + Fn() -> String + 'static>(self, context: C) -> Result<T> {
+ self.with_context_kind(context, ErrorKind::Unrecoverable)
+ }
+ fn kind(self, kind: ErrorKind) -> Result<T>;
+}
+
+impl<T> Context<T> for Option<T> {
+ fn context_kind<C: Display + Send + 'static>(self, context: C, kind: ErrorKind) -> Result<T> {
+ match self {
+ Some(ok) => Ok(ok),
+ None => Err(Error::new(
+ kind,
+ Some(context),
+ Box::new(StaticError("attempted to unwrap 'None' value")),
+ )),
+ }
+ }
+
+ fn kind(self, kind: ErrorKind) -> Result<T> {
+ match self {
+ Some(ok) => Ok(ok),
+ None => Err(Error::new::<&str>(
+ kind,
+ None,
+ Box::new(StaticError("attempted to unwrap 'None' value")),
+ )),
+ }
+ }
+}
+
+impl<T, E: std::error::Error + Send + 'static> Context<T> for Result<T, E> {
+ fn context_kind<C: Display + Send + 'static>(self, context: C, kind: ErrorKind) -> Result<T> {
+ match self {
+ Ok(ok) => Ok(ok),
+ Err(e) => Err(Error::new(kind, Some(context), Box::new(e))),
+ }
+ }
+
+ fn kind(self, kind: ErrorKind) -> Result<T> {
+ match self {
+ Ok(ok) => Ok(ok),
+ Err(e) => Err(Error::new::<&str>(kind, None, Box::new(e))),
+ }
+ }
+}
diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod constants;
+pub mod error;
diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "kanit-diagnostics"
+version.workspace = true
+edition.workspace = true
+
+[features]
+tap = []
+timings = ["send_wrapper"]
+
+[dependencies.send_wrapper]
+version = "0.6"
+optional = true
+
+[dependencies.log]
+version = "0.4"
+features = ["std"]
+
+[dependencies.kanit-common]
+path = "../common"
diff --git a/crates/diagnostics/src/lib.rs b/crates/diagnostics/src/lib.rs
@@ -0,0 +1,44 @@
+pub use logger::*;
+
+mod logger;
+mod scope;
+#[cfg(feature = "tap")]
+pub mod tap;
+#[cfg(feature = "timings")]
+pub mod timing;
+
+#[cfg(not(feature = "tap"))]
+pub mod tap {
+ pub fn header() {}
+
+ pub fn enter_subtest<S: ToString>(_: Option<S>) {}
+
+ pub fn exit_subtest() {}
+
+ pub fn plan(_: usize) {}
+
+ pub fn bail<S: ToString>(_: Option<S>) {}
+
+ pub fn ok<S: ToString>(_: usize, _: Option<S>) {}
+
+ pub fn not_ok<S: ToString>(_: usize, _: Option<S>) {}
+}
+
+#[cfg(not(feature = "timings"))]
+pub mod timing {
+ use std::rc::Rc;
+
+ pub use crate::scope::Scope;
+
+ pub fn register() {}
+
+ pub fn push_scope<S: ToString>(_name: S) -> usize {
+ 0
+ }
+
+ pub fn pop_scope(_id: usize) {}
+
+ pub fn get_scopes() -> Rc<[Scope]> {
+ Rc::from([])
+ }
+}
diff --git a/crates/diagnostics/src/logger.rs b/crates/diagnostics/src/logger.rs
@@ -0,0 +1,92 @@
+use std::io::Write;
+use std::{fmt, io};
+
+use log::{LevelFilter, Metadata, Record, set_boxed_logger, set_max_level};
+
+use kanit_common::error::{Context, Result};
+
+const SYMBOLS: [char; 5] = ['!', '=', '*', '&', '#'];
+
+const COLORS: [Colors; 5] = [
+ Colors::BrightRed,
+ Colors::BrightYellow,
+ Colors::BrightGreen,
+ Colors::BrightCyan,
+ Colors::BrightMagenta,
+];
+
+#[repr(u8)]
+#[derive(Copy, Clone)]
+pub enum Colors {
+ Black = 0,
+ Red = 1,
+ Green = 2,
+ Yellow = 3,
+ Blue = 4,
+ Magenta = 5,
+ Cyan = 6,
+ White = 7,
+ BrightBlack = 8,
+ BrightRed = 9,
+ BrightGreen = 10,
+ BrightYellow = 11,
+ BrightBlue = 12,
+ BrightMagenta = 13,
+ BrightCyan = 14,
+ BrightWhite = 15,
+}
+
+impl Colors {
+ pub fn reset() -> &'static str {
+ "\x1b[0m"
+ }
+}
+
+impl fmt::Display for Colors {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "\x1b[38;5;{}m", *self as u16)
+ }
+}
+
+// temporary solution
+// logger should switch to a journal once initialized
+pub struct InitializationLogger {
+ level: LevelFilter,
+}
+
+impl InitializationLogger {
+ pub fn init(level: LevelFilter) -> Result<()> {
+ let logger = Self { level };
+
+ set_max_level(level);
+ set_boxed_logger(Box::new(logger)).context("failed to initialize logger")?;
+
+ Ok(())
+ }
+}
+
+impl log::Log for InitializationLogger {
+ fn enabled(&self, metadata: &Metadata) -> bool {
+ metadata.level() <= self.level
+ }
+
+ fn log(&self, record: &Record) {
+ if self.enabled(record.metadata()) {
+ let idx = (record.level() as usize) - 1;
+ let sym = SYMBOLS[idx];
+ let color = COLORS[idx];
+
+ let mut stdout = io::stdout().lock();
+
+ // we can't handle error
+ let _ = writeln!(
+ &mut stdout,
+ "{color}{sym}{} {}",
+ Colors::reset(),
+ record.args()
+ );
+ }
+ }
+
+ fn flush(&self) {}
+}
diff --git a/crates/diagnostics/src/scope.rs b/crates/diagnostics/src/scope.rs
@@ -0,0 +1,9 @@
+use std::time::{Duration, Instant};
+
+#[derive(Clone)]
+pub struct Scope {
+ pub name: String,
+ pub start: Instant,
+ pub duration: Option<Duration>,
+ pub level: usize,
+}
diff --git a/crates/diagnostics/src/tap.rs b/crates/diagnostics/src/tap.rs
@@ -0,0 +1,57 @@
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+static LEVEL: AtomicUsize = AtomicUsize::new(0);
+
+fn print_leveled<S: ToString>(s: S) {
+ println!(
+ "{}{}",
+ " ".repeat(LEVEL.load(Ordering::Relaxed)),
+ s.to_string()
+ );
+}
+
+pub fn header() {
+ println!("TAP version 14");
+}
+
+pub fn enter_subtest<S: ToString>(desc: Option<S>) {
+ if let Some(desc) = desc {
+ print_leveled(format!("# Subtest: {}", desc.to_string()))
+ } else {
+ print_leveled("# Subtest")
+ }
+
+ LEVEL.fetch_add(1, Ordering::Relaxed);
+}
+
+pub fn exit_subtest() {
+ LEVEL.fetch_sub(1, Ordering::Relaxed);
+}
+
+pub fn plan(tests: usize) {
+ print_leveled(format!("1..{tests}"))
+}
+
+pub fn bail<S: ToString>(desc: Option<S>) {
+ if let Some(desc) = desc {
+ print_leveled(format!("Bail out! {}", desc.to_string()))
+ } else {
+ print_leveled("Bail out!");
+ }
+}
+
+pub fn ok<S: ToString>(test: usize, desc: Option<S>) {
+ if let Some(desc) = desc {
+ print_leveled(format!("ok {test} - {}", desc.to_string()));
+ } else {
+ print_leveled(format!("ok {test}"));
+ }
+}
+
+pub fn not_ok<S: ToString>(test: usize, desc: Option<S>) {
+ if let Some(desc) = desc {
+ print_leveled(format!("not ok {test} - {}", desc.to_string()));
+ } else {
+ print_leveled(format!("not ok {test}"));
+ }
+}
diff --git a/crates/diagnostics/src/timing.rs b/crates/diagnostics/src/timing.rs
@@ -0,0 +1,66 @@
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::sync::OnceLock;
+use std::time::Instant;
+
+use send_wrapper::SendWrapper;
+
+pub use crate::scope::Scope;
+
+static GLOBAL_TIMER: OnceLock<SendWrapper<RefCell<Timer>>> = OnceLock::new();
+
+pub struct Timer {
+ scopes: Vec<Scope>,
+ level: usize,
+}
+
+pub fn register() {
+ let _ = GLOBAL_TIMER.set(SendWrapper::new(RefCell::new(Timer {
+ scopes: vec![],
+ level: 0,
+ })));
+}
+
+pub fn push_scope<S: ToString>(name: S) -> Option<usize> {
+ if let Some(timer) = GLOBAL_TIMER.get() {
+ let level = timer.borrow().level;
+ let mut timer = timer.borrow_mut();
+
+ timer.scopes.push(Scope {
+ name: name.to_string(),
+ start: Instant::now(),
+ duration: None,
+ level,
+ });
+
+ let id = timer.scopes.len() - 1;
+
+ timer.level += 1;
+
+ Some(id)
+ } else {
+ None
+ }
+}
+
+pub fn pop_scope(id: Option<usize>) {
+ if let Some(timer) = GLOBAL_TIMER.get() {
+ let mut timer = timer.borrow_mut();
+
+ if let Some(id) = id {
+ let scope = &mut timer.scopes[id];
+
+ scope.duration = Some(Instant::now() - scope.start);
+ }
+
+ timer.level -= 1;
+ }
+}
+
+pub fn get_scopes() -> Rc<[Scope]> {
+ if let Some(timer) = GLOBAL_TIMER.get() {
+ timer.borrow().scopes.clone().into()
+ } else {
+ Rc::from([])
+ }
+}
diff --git a/crates/executor/Cargo.toml b/crates/executor/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "kanit-executor"
+version.workspace = true
+edition.workspace = true
+
+[dependencies.futures-lite]
+version = "2.3"
+
+[dependencies.send_wrapper]
+version = "0.6"
+
+[dependencies.async-io]
+version = "2.3"
+
+[dependencies.async-executor]
+version = "1.12"
diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs
@@ -0,0 +1,33 @@
+use std::future::Future;
+use std::sync::OnceLock;
+
+use async_executor::{LocalExecutor, Task};
+use futures_lite::StreamExt;
+use futures_lite::stream::iter;
+use send_wrapper::SendWrapper;
+
+static GLOBAL_EXECUTOR: OnceLock<SendWrapper<LocalExecutor<'static>>> = OnceLock::new();
+
+pub fn spawn<T: 'static>(future: impl Future<Output = T> + 'static) -> Task<T> {
+ GLOBAL_EXECUTOR
+ .get_or_init(|| SendWrapper::new(LocalExecutor::new()))
+ .spawn(future)
+}
+
+pub fn block<T: 'static>(future: impl Future<Output = T> + 'static) -> T {
+ async_io::block_on(
+ GLOBAL_EXECUTOR
+ .get_or_init(|| SendWrapper::new(LocalExecutor::new()))
+ .run(future),
+ )
+}
+
+pub async fn join_all<I, F, R: 'static>(futures: I) -> Vec<R>
+where
+ I: IntoIterator<Item = F>,
+ F: Future<Output = R> + 'static,
+{
+ let handles: Vec<_> = futures.into_iter().map(spawn).collect();
+
+ iter(handles).then(|f| f).collect().await
+}
diff --git a/crates/init/Cargo.toml b/crates/init/Cargo.toml
@@ -0,0 +1,48 @@
+[package]
+name = "kanit-init"
+version.workspace = true
+edition.workspace = true
+
+[features]
+baked-rc = ["kanit-rc"]
+timings = ["kanit-diagnostics/timings"]
+testing = ["kanit-diagnostics/tap", "kanit-rc?/testing"]
+
+[dependencies.futures-lite]
+version = "2.3"
+
+[dependencies.async-fs]
+version = "2.1"
+
+[dependencies.async-signal]
+version = "0.2"
+
+[dependencies.nix]
+version = "0.29"
+features = [
+ "reboot",
+ "signal",
+ "fs",
+ "feature",
+ "process",
+ "ioctl"
+]
+
+[dependencies.libc]
+version = "0.2"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.kanit-diagnostics]
+path = "../diagnostics"
+
+[dependencies.kanit-executor]
+path = "../executor"
+
+[dependencies.kanit-common]
+path = "../common"
+
+[dependencies.kanit-rc]
+path = "../rc"
+optional = true
diff --git a/crates/init/src/baked_rc.rs b/crates/init/src/baked_rc.rs
@@ -0,0 +1,20 @@
+use kanit_common::error::Result;
+use kanit_executor::block;
+
+pub fn teardown_rc() -> Result<()> {
+ block(kanit_rc::teardown())?;
+
+ Ok(())
+}
+
+pub fn initialize_rc() -> Result<()> {
+ block(kanit_rc::start())?;
+
+ Ok(())
+}
+
+#[inline]
+#[cfg(not(feature = "testing"))]
+pub async fn event_rc(ev: Vec<u8>) -> Result<()> {
+ kanit_rc::event(ev).await
+}
diff --git a/crates/init/src/bsod.rs b/crates/init/src/bsod.rs
@@ -0,0 +1,111 @@
+// we all love to see a bsod
+
+use std::backtrace::Backtrace;
+use std::io::{Write, stdin, stdout};
+use std::os::fd::RawFd;
+use std::os::unix::process::CommandExt;
+use std::panic::PanicHookInfo;
+use std::process::Command;
+
+use nix::fcntl::{OFlag, open};
+use nix::ioctl_write_int_bad;
+use nix::sys::reboot::{RebootMode, reboot};
+use nix::sys::stat::Mode;
+use nix::unistd::isatty;
+
+const BLUE_BG: &str = "\x1b]P0000080";
+const WHITE_TEXT: &str = "\x1b]P7FFFFFF";
+
+const CLEAR_SCREEN: &str = "\x1b[1;1H\x1b[2J";
+
+const VT_ACTIVATE: i32 = 0x5606;
+const VT_WAIT_ACTIVE: i32 = 0x5607;
+
+ioctl_write_int_bad!(vt_activate, VT_ACTIVATE);
+ioctl_write_int_bad!(vt_wait_activate, VT_WAIT_ACTIVE);
+
+fn print_info(info: &PanicHookInfo, tty: bool) {
+ let mut stdout = stdout();
+
+ if tty {
+ let _ = write!(&mut stdout, "{BLUE_BG}{WHITE_TEXT}{CLEAR_SCREEN}");
+ }
+
+ let _ = writeln!(
+ &mut stdout,
+ "Kanit Panic\n===========\nKanit experienced an unrecoverable error and cannot continue."
+ );
+ let _ = writeln!(
+ &mut stdout,
+ "Consider checking the system journal or the following backtrace.\n"
+ );
+
+ if let Some(s) = info.payload().downcast_ref::<&str>() {
+ let _ = writeln!(&mut stdout, "Message: {s}");
+ }
+
+ if let Some(loc) = info.location() {
+ let _ = writeln!(
+ &mut stdout,
+ "File: {}:{}:{}",
+ loc.file(),
+ loc.line(),
+ loc.column()
+ );
+ }
+
+ let mut junk = String::new();
+
+ if tty {
+ let _ = writeln!(&mut stdout, "Press enter to view backtrace...");
+
+ let _ = stdout.flush();
+
+ let _ = stdin().read_line(&mut junk);
+ }
+
+ let backtrace = Backtrace::force_capture();
+
+ let _ = writeln!(&mut stdout, "-- backtrace --\n{backtrace}");
+
+ if tty {
+ let _ = writeln!(
+ &mut stdout,
+ "Init is now considered unstable, press enter to enter into emergency shell..."
+ );
+
+ let _ = stdout.flush();
+ let _ = stdin().read_line(&mut junk);
+ }
+
+ let _ = Command::new("sh").exec();
+
+ if tty {
+ let _ = writeln!(
+ &mut stdout,
+ "failed to enter emergency shell, press enter to restart..."
+ );
+
+ let _ = stdout.flush();
+ let _ = stdin().read_line(&mut junk);
+ }
+
+ let _ = reboot(RebootMode::RB_AUTOBOOT);
+}
+
+pub fn bsod(info: &PanicHookInfo) {
+ if !isatty(RawFd::from(1)).unwrap_or(false) {
+ return print_info(info, false);
+ }
+
+ // change virtual terminal
+ if let Ok(fd) = open("/dev/console", OFlag::O_RDONLY, Mode::empty()) {
+ // SAFETY: we ensure `fd` is valid
+ unsafe {
+ let _ = vt_activate(fd, 1);
+ let _ = vt_wait_activate(fd, 1);
+ }
+ }
+
+ print_info(info, false);
+}
diff --git a/crates/init/src/ev_loop.rs b/crates/init/src/ev_loop.rs
@@ -0,0 +1,89 @@
+use async_fs::File;
+use async_signal::{Signal, Signals};
+use futures_lite::{AsyncReadExt, StreamExt};
+use log::debug;
+use nix::errno::Errno;
+use nix::sys::reboot::RebootMode;
+use nix::sys::stat::Mode;
+use nix::unistd::mkfifo;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result};
+use kanit_executor::{block, spawn};
+
+use crate::{event_rc, teardown};
+
+async fn pipe_handler(data: Vec<u8>) -> Result<()> {
+ if data.starts_with(b"halt") {
+ teardown(Some(RebootMode::RB_HALT_SYSTEM))?;
+ } else if data.starts_with(b"poweroff") {
+ teardown(Some(RebootMode::RB_POWER_OFF))?;
+ } else if data.starts_with(b"reboot") {
+ teardown(Some(RebootMode::RB_AUTOBOOT))?;
+ } else if data.starts_with(b"kexec") {
+ teardown(Some(RebootMode::RB_KEXEC))?;
+ } else {
+ event_rc(data).await?;
+ }
+
+ Ok(())
+}
+
+async fn listen_signal() -> Result<()> {
+ let mut signals = Signals::new([Signal::Int, Signal::Term, Signal::Child])
+ .context("failed to register signals")?;
+
+ while let Some(signal) = signals.next().await {
+ let signal = if let Ok(signal) = signal {
+ signal
+ } else {
+ continue;
+ };
+
+ debug!("*boop* {signal:?}");
+ }
+
+ Ok(())
+}
+
+async fn listen_file() -> Result<()> {
+ mkfifo(constants::KAN_PIPE, Mode::S_IRUSR | Mode::S_IWUSR).context("failed to create pipe")?;
+
+ loop {
+ let mut buff = vec![0u8; 512];
+
+ let mut file = match File::open(constants::KAN_PIPE).await {
+ Ok(f) => f,
+ Err(e) => match e.raw_os_error() {
+ None => return Err(e.into()),
+ Some(n) => {
+ if Errno::from_raw(n) == Errno::EINTR {
+ continue;
+ } else {
+ return Err(e.into());
+ }
+ }
+ },
+ };
+
+ file.read(&mut buff).await.context("failed to read pipe")?;
+
+ spawn(pipe_handler(buff)).detach();
+ }
+}
+
+async fn inner_ev_loop() -> Result<()> {
+ let handles = [spawn(listen_signal()), spawn(listen_file())];
+
+ for handle in handles {
+ handle.await?;
+ }
+
+ Ok(())
+}
+
+pub fn ev_loop() -> Result<()> {
+ block(inner_ev_loop())?;
+
+ Ok(())
+}
diff --git a/crates/init/src/lib.rs b/crates/init/src/lib.rs
@@ -0,0 +1,213 @@
+#[cfg(feature = "timings")]
+use std::fs::File;
+#[cfg(not(feature = "testing"))]
+use std::io;
+#[cfg(feature = "timings")]
+use std::io::Write;
+#[cfg(not(feature = "testing"))]
+use std::os::unix::process::CommandExt;
+#[cfg(not(feature = "testing"))]
+use std::process::Command;
+use std::process::ExitCode;
+use std::thread::sleep;
+use std::time::Duration;
+use std::{env, panic, process};
+
+#[cfg(not(feature = "testing"))]
+use log::LevelFilter;
+#[cfg(feature = "timings")]
+use log::warn;
+use log::{error, info};
+use nix::sys::reboot::{RebootMode, reboot, set_cad_enabled};
+use nix::sys::signal::{SigSet, Signal, kill};
+#[cfg(not(feature = "testing"))]
+use nix::sys::utsname::uname;
+use nix::unistd::{Pid, sync};
+
+#[cfg(feature = "baked-rc")]
+use baked_rc::*;
+#[cfg(not(feature = "testing"))]
+use ev_loop::ev_loop;
+use kanit_common::constants;
+use kanit_common::error::{Context, Result};
+#[cfg(not(feature = "testing"))]
+use kanit_diagnostics::Colors;
+#[cfg(feature = "testing")]
+use kanit_diagnostics::tap as kanit_tap;
+use kanit_diagnostics::timing as kanit_timing;
+#[cfg(feature = "timings")]
+use kanit_timing::Scope;
+#[cfg(not(feature = "baked-rc"))]
+use rc::*;
+
+#[cfg(feature = "baked-rc")]
+mod baked_rc;
+#[cfg(not(feature = "testing"))]
+mod bsod;
+#[cfg(not(feature = "testing"))]
+mod ev_loop;
+#[cfg(not(feature = "baked-rc"))]
+mod rc;
+
+#[cfg(feature = "timings")]
+fn write_scope(file: &mut File, scope: &Scope) -> Result<()> {
+ let scope_fmt = format!(
+ "{} {} {}\n",
+ scope.name,
+ scope.duration.unwrap_or(Duration::from_secs(0)).as_micros(),
+ scope.level
+ );
+
+ file.write(scope_fmt.as_bytes())
+ .context("failed to write scope")?;
+ Ok(())
+}
+
+#[cfg(feature = "timings")]
+fn write_timing() -> Result<()> {
+ let mut file = File::create(constants::KAN_TIMINGS).context("failed to open times file")?;
+
+ for scope in kanit_timing::get_scopes().iter() {
+ write_scope(&mut file, scope)?;
+ }
+
+ Ok(())
+}
+
+fn initialize() -> Result<()> {
+ let id = kanit_timing::push_scope("initialize");
+
+ #[cfg(not(feature = "testing"))]
+ let platform = uname()
+ .map(|u| {
+ format!(
+ "{} {} ({})",
+ u.sysname().to_string_lossy(),
+ u.release().to_string_lossy(),
+ u.machine().to_string_lossy()
+ )
+ })
+ .unwrap_or_else(|_| "unknown".to_string());
+
+ #[cfg(not(feature = "testing"))]
+ info!(
+ "== Kanit {}{}{} on {}{}{} ==\n",
+ Colors::BrightGreen,
+ constants::KAN_VERSION,
+ Colors::reset(),
+ Colors::BrightMagenta,
+ platform,
+ Colors::reset()
+ );
+
+ let mut set = SigSet::all();
+ set.remove(Signal::SIGCHLD);
+
+ set.thread_block().context("failed to block signals")?;
+
+ set_cad_enabled(false).context("failed to ignore CAD")?;
+
+ // SAFETY: single threaded
+ unsafe {
+ env::set_var("PATH", constants::KAN_PATH);
+ }
+
+ initialize_rc()?;
+
+ kanit_timing::pop_scope(id);
+
+ #[cfg(feature = "timings")]
+ if let Err(e) = write_timing() {
+ warn!("failed to write timings: {e}");
+ };
+
+ Ok(())
+}
+
+pub fn teardown(cmd: Option<RebootMode>) -> Result<()> {
+ teardown_rc()?;
+
+ info!("terminating remaining processes");
+
+ kill(Pid::from_raw(-1), Signal::SIGTERM).context("failed to terminate all processes")?;
+
+ sleep(Duration::from_secs(3));
+
+ info!("killing remaining processes");
+
+ kill(Pid::from_raw(-1), Signal::SIGKILL).context("failed to kill all processes")?;
+
+ sync();
+
+ // reboot will always return an error
+ if let Some(cmd) = cmd {
+ let _ = reboot(cmd);
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "testing")]
+fn failure_handle() {
+ kanit_tap::bail::<&str>(None);
+ let _ = reboot(RebootMode::RB_POWER_OFF);
+}
+
+#[cfg(not(feature = "testing"))]
+fn failure_handle() {
+ error!("dropping into emergency shell");
+
+ let _ = Command::new("sh").exec();
+
+ error!("failed to drop into emergency shell; good luck o7");
+
+ loop {
+ // kernel panic if this loop doesn't exist
+ sleep(Duration::from_secs(1));
+ }
+}
+
+pub fn handle_cli() -> ExitCode {
+ if process::id() != 1 {
+ eprintln!("init must be ran as PID 1");
+ return ExitCode::FAILURE;
+ }
+
+ #[cfg(feature = "testing")]
+ panic::set_hook(Box::new(|info| {
+ kanit_tap::bail(info.payload().downcast_ref::<&str>());
+ let _ = reboot(RebootMode::RB_POWER_OFF);
+ }));
+
+ #[cfg(not(feature = "testing"))]
+ panic::set_hook(Box::new(|info| {
+ error!("panic detected; tearing down");
+ let _ = teardown(None); // attempt a teardown
+ bsod::bsod(info);
+ }));
+
+ kanit_diagnostics::tap::header();
+
+ #[cfg(not(feature = "testing"))]
+ if let Err(e) = kanit_diagnostics::InitializationLogger::init(LevelFilter::Trace) {
+ let _ = write!(&mut io::stdout().lock(), "{e}");
+ }
+
+ kanit_timing::register();
+
+ if let Err(e) = initialize() {
+ error!("failed to initialize: {e}");
+ failure_handle();
+ }
+
+ #[cfg(not(feature = "testing"))] // no way to test yet
+ if let Err(e) = ev_loop() {
+ error!("event loop failed: {e}");
+ failure_handle();
+ }
+
+ #[cfg(feature = "testing")]
+ teardown(Some(RebootMode::RB_POWER_OFF)).expect("failed to unwrap");
+
+ ExitCode::SUCCESS
+}
diff --git a/crates/init/src/main.rs b/crates/init/src/main.rs
@@ -0,0 +1,5 @@
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+ kanit_init::handle_cli()
+}
diff --git a/crates/init/src/rc.rs b/crates/init/src/rc.rs
@@ -0,0 +1,42 @@
+use std::path::Path;
+use std::process::Command;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+
+pub fn initialize_rc() -> Result<()> {
+ if !Path::new("/etc/rc.start").exists() {
+ Err(StaticError("failed to find a start script")).kind(ErrorKind::Unrecoverable)?;
+ }
+
+ Command::new("/etc/rc.start")
+ .spawn()
+ .context("failed to start rc.start")?;
+
+ Ok(())
+}
+
+pub fn teardown_rc() -> Result<()> {
+ if !Path::new("/etc/rc.stop").exists() {
+ Err(StaticError("failed to find a stop script")).kind(ErrorKind::Unrecoverable)?;
+ }
+
+ Command::new("/etc/rc.stop")
+ .spawn()
+ .context("failed to start rc.start")?;
+
+ Ok(())
+}
+
+#[cfg(not(feature = "testing"))]
+pub async fn event_rc(ev: Vec<u8>) -> Result<()> {
+ if !Path::new("/etc/rc.event").exists() {
+ return Ok(());
+ }
+
+ Command::new("/etc/rc.event")
+ .arg(String::from_utf8_lossy(&ev).to_string())
+ .spawn()
+ .context_kind("failed to start rc.event", ErrorKind::Recoverable)?;
+
+ Ok(())
+}
diff --git a/crates/rc/Cargo.toml b/crates/rc/Cargo.toml
@@ -0,0 +1,58 @@
+[package]
+name = "kanit-rc"
+version.workspace = true
+edition.workspace = true
+
+[features]
+default = ["units"]
+units = ["kanit-units"]
+testing = ["kanit-units?/testing"]
+
+[dependencies.sha2]
+version = "0.10"
+
+[dependencies.walkdir]
+version = "2.5"
+
+[dependencies.send_wrapper]
+version = "0.6"
+
+[dependencies.async-lock]
+version = "3.4"
+
+[dependencies.async-process]
+version = "2.2"
+
+[dependencies.async-trait]
+version = "0.1"
+
+[dependencies.log]
+version = "0.4"
+features = ["std"]
+
+[dependencies.nix]
+version = "0.29"
+features = [
+ "signal",
+ "process"
+]
+
+[dependencies.kanit-units]
+path = "../units"
+optional = true
+
+[dependencies.kanit-unit]
+path = "../unit"
+
+[dependencies.kanit-diagnostics]
+path = "../diagnostics"
+
+[dependencies.kanit-executor]
+path = "../executor"
+
+[dependencies.kanit-supervisor]
+path = "../supervisor"
+
+[dependencies.kanit-common]
+path = "../common"
+
diff --git a/crates/rc/src/control.rs b/crates/rc/src/control.rs
@@ -0,0 +1,217 @@
+#[cfg(not(feature = "testing"))]
+use std::io::{Write, stdin, stdout};
+
+use async_process::driver;
+use log::{debug, error, info, warn};
+
+#[cfg(not(feature = "testing"))]
+use kanit_common::error::Context;
+use kanit_common::error::{Error, Result};
+use kanit_diagnostics::tap as kanit_tap;
+use kanit_diagnostics::timing as kanit_timing;
+use kanit_executor::{join_all, spawn};
+use kanit_unit::{RcUnit, UnitName};
+
+use crate::loader;
+use crate::loader::Loader;
+
+#[cfg(not(feature = "testing"))]
+fn critical_unit_fail(err: Error) -> Result<()> {
+ loop {
+ error!("a critical unit failed to start, continue? [y/n]: ");
+
+ let _ = stdout().flush();
+
+ let mut input = String::new();
+
+ match stdin().read_line(&mut input) {
+ Ok(_) => match input.chars().next() {
+ Some('y') => return Ok(()),
+ Some('n') => return Err(err),
+ _ => {}
+ },
+ Err(e) => return Err(e).context("failed to read stdin"),
+ }
+ }
+}
+
+#[cfg(feature = "testing")]
+fn critical_unit_fail(err: Error) -> Result<()> {
+ kanit_tap::bail(Some(err.to_string()));
+
+ Err(err)
+}
+
+async fn start_unit(tuple: (usize, RcUnit)) -> Result<Option<UnitName>> {
+ let (j, unit) = tuple;
+
+ let mut unit_b = unit.borrow_mut();
+
+ debug!("loading unit {}", unit_b.name());
+
+ let id = kanit_timing::push_scope(format!("unit:{}", unit_b.name()));
+
+ if !unit_b.prepare().await? {
+ warn!("failed preparations for {}", unit_b.name());
+ kanit_tap::not_ok(j + 1, Some("failed preparations"));
+ return Ok(None);
+ }
+
+ if let Err(e) = unit_b.start().await {
+ if e.is_recoverable() {
+ warn!("{e}");
+ kanit_tap::not_ok(j + 1, Some(e));
+ } else {
+ error!("{e}");
+ critical_unit_fail(e)?;
+ }
+
+ kanit_timing::pop_scope(id);
+
+ return Ok(None);
+ }
+
+ kanit_timing::pop_scope(id);
+
+ kanit_tap::ok(j + 1, Some(unit_b.name()));
+
+ debug!("finished loading unit {}", unit_b.name());
+
+ Ok(Some(unit_b.name().clone()))
+}
+
+async fn start_level(i: usize, name: Box<str>) -> Result<()> {
+ let mut loader = Loader::obtain()?.borrow_mut();
+ let level = loader.grouping.get(&name).cloned().unwrap_or_else(Vec::new);
+
+ info!("starting level {name}");
+
+ let scope_str = format!("level:{name}");
+
+ kanit_tap::enter_subtest(Some(&scope_str));
+
+ let id = kanit_timing::push_scope(&scope_str);
+
+ kanit_tap::plan(level.len());
+
+ for (j, group) in level.into_iter().enumerate() {
+ let group_str = format!("group:{j}");
+
+ kanit_tap::enter_subtest(Some(&group_str));
+ kanit_tap::plan(group.len());
+
+ let handles = join_all(group.into_iter().enumerate().map(start_unit)).await;
+
+ for handle in handles {
+ if let Some(n) = handle? {
+ loader.mark_started(n);
+ }
+ }
+
+ kanit_tap::exit_subtest();
+ kanit_tap::ok(j + 1, Some(&group_str));
+ }
+
+ kanit_timing::pop_scope(id);
+
+ kanit_tap::exit_subtest();
+ kanit_tap::ok(i + 1, Some(&scope_str));
+
+ Ok(())
+}
+
+pub async fn start() -> Result<()> {
+ kanit_timing::register();
+
+ loader::init_loader()?;
+
+ // include teardown as well
+ // sysboot, boot, default * 2
+ kanit_tap::plan(6);
+
+ let driver_task = spawn(driver());
+
+ start_level(0, Box::from("sysboot")).await?;
+ start_level(1, Box::from("boot")).await?;
+ start_level(2, Box::from("default")).await?;
+
+ driver_task.cancel().await;
+
+ {
+ let loader = Loader::obtain()?;
+
+ let _ = loader.borrow().save();
+ }
+
+ Ok(())
+}
+
+async fn stop_unit(tuple: (usize, RcUnit)) -> Result<()> {
+ let (j, unit) = tuple;
+
+ let mut unit_b = unit.borrow_mut();
+
+ debug!("unloading unit {}", unit_b.name());
+
+ match unit_b.stop().await {
+ Ok(_) => kanit_tap::ok(j + 1, Some(unit_b.name())),
+ Err(e) => {
+ kanit_tap::not_ok(j + 1, Some(unit_b.name()));
+
+ if e.is_recoverable() {
+ warn!("{e}")
+ } else {
+ error!("{e}") // won't stop still
+ }
+ }
+ }
+
+ debug!("finished unloading unit {}", unit_b.name());
+
+ Ok(())
+}
+
+async fn teardown_level(total: usize, i: usize, name: Box<str>) -> Result<()> {
+ let loader = Loader::obtain()?.borrow();
+ let level = loader.grouping.get(&name).cloned().unwrap_or_else(Vec::new);
+
+ info!("stopping level {name}");
+
+ let scope_str = format!("level:{name}-stop");
+ kanit_tap::enter_subtest(Some(&scope_str));
+
+ kanit_tap::plan(level.len());
+
+ for (j, group) in level.into_iter().enumerate() {
+ let group_str = format!("group:{j}");
+
+ kanit_tap::enter_subtest(Some(&group_str));
+ kanit_tap::plan(group.len());
+
+ let handles = join_all(group.into_iter().enumerate().map(stop_unit)).await;
+
+ for handle in handles {
+ handle?;
+ }
+
+ kanit_tap::exit_subtest();
+ kanit_tap::ok(j + 1, Some(&group_str));
+ }
+
+ kanit_tap::exit_subtest();
+ kanit_tap::ok(total + (total - i), Some(&scope_str));
+
+ Ok(())
+}
+
+pub async fn teardown() -> Result<()> {
+ let driver_task = spawn(driver());
+
+ teardown_level(3, 2, Box::from("default")).await?;
+ teardown_level(3, 1, Box::from("boot")).await?;
+ teardown_level(3, 0, Box::from("sysboot")).await?;
+
+ driver_task.cancel().await;
+
+ Ok(())
+}
diff --git a/crates/rc/src/event.rs b/crates/rc/src/event.rs
@@ -0,0 +1,143 @@
+use std::collections::HashSet;
+
+use kanit_common::error::{Context, Result, StaticError};
+use kanit_unit::UnitName;
+use log::warn;
+
+use crate::loader::Loader;
+
+async fn modify_service(start: bool, level: usize, name: &[u8]) -> Result<()> {
+ // this is horrible but it makes the compiler happy
+ let (diff, groups) = {
+ let loader = Loader::obtain()?.borrow();
+
+ let unit_name = UnitName::from(String::from_utf8_lossy(name));
+
+ if (start && loader.is_started(level, &unit_name))
+ || (!start && !loader.is_started(level, &unit_name))
+ {
+ Err(StaticError("unit already started/stopped"))?;
+ }
+
+ // rebuild database and diff to find out what needs to start
+ let mut db = loader.database().clone();
+
+ {
+ let enabled = db.enabled.get_mut(level).context("failed to get level")?;
+
+ if start {
+ enabled.insert(unit_name.clone());
+ } else {
+ enabled.remove(&unit_name);
+ }
+ }
+
+ db.rebuild_levels()?;
+
+ if !db.unit_infos.contains_key(&unit_name) {
+ Err(StaticError("failed to find unit in database"))?;
+ }
+
+ let started = loader.started.get(level).context("failed to get level")?;
+
+ // unwrap: we `get_mut` earlier
+ let enabled = db.enabled.get(level).unwrap();
+
+ let (diff, levels) = if start {
+ (
+ enabled.difference(started).cloned().collect::<HashSet<_>>(),
+ db.levels,
+ )
+ } else {
+ (
+ started.difference(enabled).cloned().collect::<HashSet<_>>(),
+ loader.database().clone().levels,
+ )
+ };
+
+ let groups = levels
+ .get(level)
+ .context("failed to get level")?
+ .get_order();
+
+ (diff, groups.clone())
+ };
+
+ let mut loader = Loader::obtain()?.borrow_mut();
+
+ if start {
+ for group in groups {
+ for unit_n in group.iter().filter(|u| diff.contains(*u)) {
+ let unit = loader.get_unit(unit_n).context("failed to get unit")?;
+
+ let mut unit_b = unit.borrow_mut();
+
+ if !unit_b.prepare().await? {
+ warn!("failed preparations for {}", unit_b.name());
+
+ continue;
+ }
+
+ if let Err(e) = unit_b.start().await {
+ warn!("{e}");
+ return Err(e);
+ } else {
+ loader.mark_started(level, unit_b.name());
+ }
+ }
+ }
+ } else {
+ for group in groups.iter().rev() {
+ for unit_n in group.iter().filter(|u| diff.contains(*u)) {
+ let unit = loader.get_unit(unit_n).context("failed to get unit")?;
+
+ let mut unit_b = unit.borrow_mut();
+
+ if let Err(e) = unit_b.stop().await {
+ warn!("{e}");
+ return Err(e);
+ } else {
+ loader.mark_stopped(level, &unit_b.name());
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+pub async fn event(data: Vec<u8>) -> Result<()> {
+ if data.starts_with(b"db-reload") {
+ let mut loader = Loader::obtain()?.borrow_mut();
+
+ let ev_lock = loader.ev_lock.clone();
+
+ let lock = ev_lock.lock().await;
+
+ loader.reload()?;
+
+ drop(lock);
+ } else if data.starts_with(b"start") || data.starts_with(b"stop") {
+ // start:tty:1
+ let mut parts = data.split(|b| *b == b':');
+
+ parts.next(); // forward start/stop
+
+ let name = parts.next().context("failed to get name")?;
+
+ let level = String::from_utf8_lossy(parts.next().context("failed to get level")?)
+ .trim()
+ .parse::<usize>()
+ .context("failed to parse level")?;
+
+ let ev_lock = Loader::obtain()?.borrow().ev_lock.clone();
+
+ let lock = ev_lock.lock().await; // get lock to ensure no one else is using the loader
+
+ modify_service(data.starts_with(b"start"), level, name).await?;
+
+ drop(lock);
+ }
+
+ Ok(())
+}
diff --git a/crates/rc/src/lib.rs b/crates/rc/src/lib.rs
@@ -0,0 +1,14 @@
+// since the loader is locked in the ev loop, only 1 reference to a unit can be held at await points
+// in start/teardown, only 1 reference is held as well
+#![allow(clippy::await_holding_refcell_ref)]
+
+pub use control::*;
+
+mod control;
+// mod event;
+mod loader;
+
+// TODO; this will be revamped anyway
+pub async fn event(_data: Vec<u8>) -> kanit_common::error::Result<()> {
+ Ok(())
+}
diff --git a/crates/rc/src/loader/mod.rs b/crates/rc/src/loader/mod.rs
@@ -0,0 +1,389 @@
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::fs;
+use std::os::unix::fs::MetadataExt;
+use std::sync::OnceLock;
+
+use log::{trace, warn};
+use send_wrapper::SendWrapper;
+use sha2::{Digest, Sha256};
+use walkdir::WalkDir;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result, StaticError};
+use kanit_unit::formats::{DependencyGrouping, Unit};
+use kanit_unit::{RcUnit, UnitName, wrap_unit};
+use sort::{SortableUnit, obtain_load_order};
+
+mod sort;
+
+static LOADER: OnceLock<SendWrapper<RefCell<Loader>>> = OnceLock::new();
+
+pub struct Loader {
+ pub started: HashSet<UnitName>, // TODO; should become statuses
+ pub enabled: HashMap<Box<str>, HashSet<UnitName>>,
+ pub grouping: HashMap<Box<str>, Vec<Vec<RcUnit>>>,
+ pub units: HashMap<UnitName, RcUnit>,
+ // pub ev_lock: Rc<Mutex<()>>, // i am pro at rust
+}
+
+enum UnitLoad {
+ FailedRead(String),
+ FailedParse(String),
+ Loaded(Box<Unit>),
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+enum DrillRound {
+ All,
+ Positional,
+ Needs,
+}
+
+impl Loader {
+ // initialize populates the loader with units and if they're enabled
+ // it won't create the load order as `units` will add more units
+ pub fn initialize() -> Result<()> {
+ let mut loader = Loader {
+ started: HashSet::new(),
+ enabled: HashMap::new(),
+ grouping: HashMap::new(),
+ units: HashMap::new(),
+ // ev_lock: Rc::new(Mutex::new(())),
+ };
+
+ loader.reload()?;
+
+ let _ = LOADER.set(SendWrapper::new(RefCell::new(loader)));
+
+ Ok(())
+ }
+
+ // TODO; should we separate reload
+ pub fn reload(&mut self) -> Result<()> {
+ // TODO; async?
+ self.units = fs::read_dir(constants::KAN_UNIT_DIR)
+ .context("failed to read units directory")?
+ .filter_map(|e| e.ok())
+ .map(|f| {
+ fs::read_to_string(f.path())
+ .map(|c| {
+ (
+ UnitName::from(f.file_name().to_string_lossy()),
+ c.parse::<Unit>()
+ .map(|u| UnitLoad::Loaded(Box::new(u)))
+ .unwrap_or_else(|e| UnitLoad::FailedParse(e.to_string())),
+ )
+ })
+ .unwrap_or_else(|e| {
+ (
+ UnitName::from(f.file_name().to_string_lossy()),
+ UnitLoad::FailedRead(e.to_string()),
+ )
+ })
+ })
+ .filter_map(|(name, load)| match load {
+ UnitLoad::FailedRead(e) => {
+ warn!("failed to read unit {name}: {e}");
+ None
+ }
+ UnitLoad::FailedParse(e) => {
+ warn!("failed to parse unit {name}: {e}");
+ None
+ }
+ UnitLoad::Loaded(u) => {
+ trace!("loaded unit {name}");
+ Some((name.clone(), wrap_unit((name, *u))))
+ }
+ })
+ .collect::<HashMap<_, _>>();
+
+ // technically we don't care about the contents of the file
+ self.enabled = fs::read_dir(constants::KAN_ENABLED_DIR)
+ .context("failed to read enabled dir")?
+ .filter_map(|e| {
+ e.ok().and_then(|f| {
+ f.file_type()
+ .map(|t| t.is_dir())
+ .ok()
+ .filter(|t| *t)
+ .map(|_| f)
+ })
+ })
+ .filter_map(|f| {
+ fs::read_dir(f.path())
+ .ok()
+ .map(|r| {
+ r.filter_map(|e| e.ok())
+ .map(|e| UnitName::from(e.file_name().to_string_lossy()))
+ .collect::<HashSet<_>>()
+ })
+ .map(|e| (Box::from(f.file_name().to_string_lossy()), e))
+ })
+ .collect::<HashMap<_, _>>();
+
+ Ok(())
+ }
+
+ // rebuilding levels is such an inefficient process
+ // that's why we cache
+ // we attempt:
+ // * all requirements fulfilled (needs, wants, before, after)
+ // * no positional (needs, wants)
+ // * only needs
+ // give up if it fails
+
+ // recursively
+ fn get_edges(unit: &SortableUnit, round: DrillRound) -> Vec<UnitName> {
+ let deps = unit.dependencies.clone();
+
+ match round {
+ DrillRound::Needs => deps.needs.clone(),
+ DrillRound::Positional => deps
+ .needs
+ .iter()
+ .chain(deps.wants.iter())
+ .cloned()
+ .collect(),
+ DrillRound::All => deps
+ .needs
+ .iter()
+ .chain(deps.wants.iter())
+ .chain(deps.before.iter())
+ .chain(deps.after.iter())
+ .cloned()
+ .collect(),
+ }
+ }
+
+ fn drill(
+ to_drill: Vec<UnitName>,
+ info: &HashMap<UnitName, SortableUnit>,
+ round: DrillRound,
+ seen: &mut HashSet<UnitName>,
+ ) -> Result<()> {
+ for unit in to_drill {
+ if seen.contains(&unit) {
+ continue;
+ }
+
+ seen.insert(unit.clone());
+
+ let unit_info = info
+ .get(&unit)
+ .with_context(move || format!("failed to find unit `{unit}`"))?;
+
+ Self::drill(Self::get_edges(unit_info, round), info, round, seen)?;
+ }
+
+ Ok(())
+ }
+
+ fn rebuild_level(
+ &self,
+ info: &HashMap<UnitName, SortableUnit>,
+ enabled: &HashSet<UnitName>,
+ ) -> Result<Vec<Vec<RcUnit>>> {
+ let mut to_load = HashSet::new();
+
+ let rounds = [DrillRound::All, DrillRound::Positional, DrillRound::Needs];
+
+ for round in rounds {
+ if Self::drill(enabled.iter().cloned().collect(), info, round, &mut to_load).is_err() {
+ continue;
+ }
+
+ let units = to_load
+ .iter()
+ .filter_map(|n| info.get(n))
+ .cloned()
+ .collect();
+
+ match obtain_load_order(units) {
+ Ok(order) => {
+ return Ok(order
+ .iter()
+ .map(|n| {
+ n.iter()
+ .map(|m| self.units.get(&m.name).cloned().unwrap())
+ .collect::<Vec<_>>()
+ })
+ .collect::<Vec<_>>());
+ }
+ Err(e) => {
+ if round == DrillRound::Needs {
+ return Err(e);
+ } else {
+ continue;
+ }
+ }
+ }
+ }
+
+ unreachable!()
+ }
+
+ fn hash_enable_dir() -> [u8; 32] {
+ WalkDir::new(constants::KAN_ENABLED_DIR)
+ .follow_links(true)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter_map(|e| e.metadata().ok().map(|e| e.mtime()))
+ .fold(Sha256::new(), |acc, m| acc.chain_update(m.to_le_bytes()))
+ .finalize()
+ .as_slice()
+ .try_into()
+ .unwrap() // sha256 hash is always 32 bytes (32 * 8 = 256)
+ }
+
+ fn try_cache_map(&mut self) -> Result<()> {
+ let grouping = fs::read_to_string(constants::KAN_DEPENDENCY_MAP)
+ .context("failed to read dependency map")?
+ .parse::<DependencyGrouping>()
+ .context("failed to parse grouping")?;
+
+ let hash = Self::hash_enable_dir();
+
+ if hash.as_slice() == grouping.hash.as_slice() {
+ self.grouping = grouping
+ .groups
+ .into_iter()
+ .map(|(k, v)| -> Result<_> {
+ Ok((
+ k,
+ v.into_iter()
+ .map(|v| {
+ v.into_iter()
+ .map(|u| {
+ self.units.get(&u).cloned().context("failed to find unit")
+ })
+ .collect::<Result<Vec<_>>>()
+ })
+ .collect::<Result<Vec<_>>>()?,
+ ))
+ })
+ .collect::<Result<HashMap<_, _>>>()?;
+ } else {
+ Err(StaticError("invalid hashes"))?;
+ }
+
+ Ok(())
+ }
+
+ pub fn rebuild(&mut self, force: bool) -> Result<()> {
+ if !force && self.try_cache_map().is_ok() {
+ return Ok(());
+ }
+
+ let info = self
+ .units
+ .iter()
+ .map(|(k, v)| (k.clone(), v.into()))
+ .collect::<HashMap<_, SortableUnit>>();
+
+ for (name, level) in self.enabled.iter() {
+ let grouping = self.rebuild_level(&info, level)?;
+
+ self.grouping.insert(name.clone(), grouping);
+ }
+
+ Ok(())
+ }
+
+ pub fn save(&self) -> Result<()> {
+ let groups = self
+ .grouping
+ .iter()
+ .map(|(k, v)| {
+ (
+ k.clone(),
+ v.iter()
+ .map(|g| g.iter().map(|u| u.borrow().name()).collect())
+ .collect(),
+ )
+ })
+ .collect();
+
+ let group = DependencyGrouping {
+ hash: Self::hash_enable_dir(),
+ groups,
+ };
+
+ fs::write(constants::KAN_DEPENDENCY_MAP, group.to_string())
+ .context("failed to write dependency map")?;
+
+ Ok(())
+ }
+
+ pub fn obtain() -> Result<&'static SendWrapper<RefCell<Self>>> {
+ let loader = LOADER.get().context("loader not initialized")?;
+
+ if !loader.valid() {
+ Err(StaticError("cannot obtain loader from different thread"))?;
+ }
+
+ Ok(loader)
+ }
+
+ pub fn extend_units<T>(&mut self, iter: T)
+ where
+ T: IntoIterator<Item = (UnitName, RcUnit)>,
+ {
+ self.units.extend(iter);
+ }
+
+ pub fn extend_enabled<T>(&mut self, iter: T)
+ where
+ T: IntoIterator<Item = (Box<str>, HashSet<UnitName>)>,
+ {
+ iter.into_iter().for_each(|(k, v)| {
+ if let Some(enabled) = self.enabled.get_mut(&k) {
+ enabled.extend(v);
+ } else {
+ self.enabled.insert(k, v);
+ }
+ })
+ }
+
+ pub fn mark_started(&mut self, name: UnitName) {
+ self.started.insert(name);
+ }
+
+ // pub fn mark_stopped(&mut self, name: &UnitName) {
+ // self.started.remove(name);
+ // }
+ //
+ // pub fn is_started(&self, name: &UnitName) -> bool {
+ // self.started.contains(name)
+ // }
+}
+
+pub fn init_loader() -> Result<()> {
+ Loader::initialize()?;
+
+ let mut loader = Loader::obtain()?.borrow_mut();
+
+ #[cfg(feature = "units")]
+ {
+ loader.extend_units(kanit_units::units().map(|u| {
+ let name = u.borrow().name();
+ (name, u)
+ }));
+
+ loader.extend_enabled(kanit_units::default_enabled());
+
+ for (unit, _) in loader.units.iter() {
+ trace!("has unit {unit}");
+ }
+
+ for (level, group) in loader.enabled.iter() {
+ group
+ .iter()
+ .for_each(|unit| trace!("enabling unit {level}->{unit}"));
+ }
+
+ loader.rebuild(false)?;
+ }
+
+ Ok(())
+}
diff --git a/crates/rc/src/loader/sort.rs b/crates/rc/src/loader/sort.rs
@@ -0,0 +1,252 @@
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use kanit_common::error::{Result, StaticError, WithError};
+use kanit_unit::{Dependencies, RcUnit, UnitName};
+
+#[derive(Debug, Clone)]
+pub struct SortableUnit {
+ pub name: UnitName,
+ pub dependencies: Rc<Dependencies>,
+}
+
+impl From<&RcUnit> for SortableUnit {
+ fn from(unit: &RcUnit) -> Self {
+ SortableUnit {
+ name: unit.borrow().name(),
+ dependencies: Rc::new(unit.borrow().dependencies()),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+struct Edge<T> {
+ from: Rc<RefCell<Node<T>>>,
+ deleted: bool,
+}
+
+#[derive(Debug)]
+struct Node<T> {
+ data: T,
+ edges: Vec<Rc<RefCell<Edge<T>>>>,
+ idx: usize,
+}
+
+pub fn obtain_load_order(units: Vec<SortableUnit>) -> Result<Vec<Vec<SortableUnit>>> {
+ let mut nodes = vec![];
+ let mut map = HashMap::new();
+
+ for (idx, unit) in units.iter().enumerate() {
+ let node = Rc::new(RefCell::new(Node {
+ data: unit.clone(),
+ edges: vec![],
+ idx,
+ }));
+
+ nodes.push(node.clone());
+ map.insert(unit.name.clone(), node);
+ }
+
+ for node in nodes.iter() {
+ let mut node_b = node.borrow_mut();
+ let dependencies = node_b.data.dependencies.clone();
+
+ for dep in dependencies.needs.iter() {
+ if let Some(unit) = map.get(&dep.clone()) {
+ let edge = Rc::new(RefCell::new(Edge {
+ from: node.clone(),
+ deleted: false,
+ }));
+
+ node_b.edges.push(edge.clone());
+ unit.borrow_mut().edges.push(edge);
+ } else {
+ let dep = dep.to_string();
+ Err(WithError::with(move || {
+ format!("failed to find needed dependency `{dep}`")
+ }))?;
+ }
+ }
+
+ for dep in dependencies.wants.iter().chain(dependencies.after.iter()) {
+ if let Some(unit) = map.get(dep) {
+ let edge = Rc::new(RefCell::new(Edge {
+ from: node.clone(),
+ deleted: false,
+ }));
+
+ node_b.edges.push(edge.clone());
+ unit.borrow_mut().edges.push(edge);
+ }
+ }
+
+ for before in dependencies.before.iter() {
+ if let Some(unit) = map.get(before) {
+ let mut unit_b = unit.borrow_mut();
+
+ let edge = Rc::new(RefCell::new(Edge {
+ from: unit.clone(),
+ deleted: false,
+ }));
+
+ node_b.edges.push(edge.clone());
+ unit_b.edges.push(edge);
+ }
+ }
+ }
+
+ let mut order = vec![];
+
+ while !nodes.is_empty() {
+ let starting_amount = nodes.len();
+ let nodes_c = nodes.clone();
+
+ let mut without_incoming: Vec<_> = nodes_c
+ .iter()
+ .filter(|n| {
+ !n.borrow().edges.iter().any(|e| {
+ let e_b = e.borrow();
+ !e_b.deleted && e_b.from.borrow().idx == n.borrow().idx
+ })
+ })
+ .collect();
+
+ let mut round = vec![];
+
+ while let Some(node) = without_incoming.pop() {
+ round.push(node.clone());
+
+ for edge in node.borrow().edges.iter() {
+ let mut edge_b = edge.borrow_mut();
+ edge_b.deleted = true;
+ }
+
+ if let Some(pos) = nodes
+ .iter()
+ .position(|n| n.borrow().idx == node.borrow().idx)
+ {
+ nodes.remove(pos);
+ }
+ }
+
+ order.push(round);
+
+ if starting_amount == nodes.len() {
+ Err(StaticError("cyclic dependency detected"))?;
+ }
+ }
+
+ Ok(order
+ .iter()
+ .map(|r| r.iter().map(|n| n.borrow().data.clone()).collect())
+ .collect())
+}
+
+// TODO; proper testing
+// these are more like a scratch pad
+#[cfg(test)]
+mod tests {
+ use async_trait::async_trait;
+
+ use kanit_unit::{Dependencies, RcUnit, Unit, UnitName, wrap_unit};
+
+ use super::*;
+
+ struct NullUnit(&'static str, Dependencies);
+
+ #[async_trait]
+ impl Unit for NullUnit {
+ fn name(&self) -> UnitName {
+ UnitName::from(self.0)
+ }
+
+ fn dependencies(&self) -> Dependencies {
+ self.1.clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ Ok(())
+ }
+ }
+
+ fn print_plan(units: Vec<Vec<SortableUnit>>) {
+ for (i, order) in units.iter().enumerate() {
+ println!("group {i}");
+ order.iter().for_each(|s| println!("|> {}", s.name));
+ }
+ }
+
+ fn to_unit_info(units: Vec<RcUnit>) -> Vec<SortableUnit> {
+ units.iter().map(SortableUnit::from).collect()
+ }
+
+ #[test]
+ fn simple_generate_order() {
+ let c = NullUnit("c", Dependencies::new());
+ let d = NullUnit("d", Dependencies::new());
+ let b = NullUnit("b", Dependencies::new().need(d.name()).clone());
+ let a = NullUnit(
+ "a",
+ Dependencies::new().need(b.name()).need(c.name()).clone(),
+ );
+
+ let units = vec![wrap_unit(a), wrap_unit(b), wrap_unit(c), wrap_unit(d)];
+
+ if let Ok(order) = obtain_load_order(to_unit_info(units)) {
+ print_plan(order);
+ }
+ }
+
+ #[test]
+ fn complex_generate_order() {
+ let f = NullUnit("2", Dependencies::new());
+ let g = NullUnit("9", Dependencies::new());
+ let h = NullUnit("10", Dependencies::new());
+ let e = NullUnit("8", Dependencies::new().need(g.name()).clone());
+
+ let c = NullUnit(
+ "3",
+ Dependencies::new().need(e.name()).need(h.name()).clone(),
+ );
+ let d = NullUnit(
+ "11",
+ Dependencies::new()
+ .need(f.name())
+ .need(g.name())
+ .need(h.name())
+ .clone(),
+ );
+ let b = NullUnit(
+ "7",
+ Dependencies::new().need(d.name()).need(e.name()).clone(),
+ );
+
+ let a = NullUnit("5", Dependencies::new().need(d.name()).clone());
+
+ let units = vec![
+ wrap_unit(a),
+ wrap_unit(b),
+ wrap_unit(c),
+ wrap_unit(d),
+ wrap_unit(e),
+ wrap_unit(f),
+ wrap_unit(g),
+ wrap_unit(h),
+ ];
+
+ if let Ok(order) = obtain_load_order(to_unit_info(units)) {
+ print_plan(order);
+ }
+ }
+
+ #[test]
+ fn cyclic_chain() {
+ let a = NullUnit("a", Dependencies::new().need(UnitName::from("b")).clone());
+ let b = NullUnit("b", Dependencies::new().need(UnitName::from("a")).clone());
+
+ let units = vec![wrap_unit(a), wrap_unit(b)];
+
+ assert!(obtain_load_order(to_unit_info(units)).is_err());
+ }
+}
diff --git a/crates/supervisor/Cargo.toml b/crates/supervisor/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "kanit-supervisor"
+version.workspace = true
+edition.workspace = true
+
+[features]
+cli = ["dep:xflags", "nix/signal"]
+
+[dependencies.async-process]
+version = "2.2"
+
+[dependencies.xflags]
+version = "0.3"
+optional = true
+
+[dependencies.nix]
+version = "0.29"
+features = ["user", "sched", "fs"]
+
+[dependencies.kanit-common]
+path = "../common"
diff --git a/crates/supervisor/src/builder.rs b/crates/supervisor/src/builder.rs
@@ -0,0 +1,161 @@
+use async_process::{Child, Command};
+
+use kanit_common::error::{Context, ErrorKind, Result};
+
+use crate::{RestartPolicy, Supervisor};
+
+pub struct SupervisorBuilder(Supervisor);
+
+#[allow(dead_code)]
+impl SupervisorBuilder {
+ pub fn new<S: ToString, I: IntoIterator<Item = S>>(cmd: S, args: I) -> Self {
+ Self(Supervisor {
+ exec: cmd.to_string(),
+ args: args.into_iter().map(|s| s.to_string()).collect(),
+ restart_delay: None,
+ restart_attempts: None,
+ restart_policy: None,
+ cgroup: None,
+ pwd: None,
+ root: None,
+ env: vec![],
+ group: None,
+ user: None,
+ stdout: None,
+ stderr: None,
+ })
+ }
+
+ pub fn from_supervisor(supervisor: Supervisor) -> Self {
+ Self(supervisor)
+ }
+
+ pub fn build(self) -> Supervisor {
+ self.0
+ }
+
+ pub fn spawn(self) -> Result<Child> {
+ let mut args = vec![];
+
+ if let Some(delay) = self.0.restart_delay {
+ args.push("-d".to_string());
+ args.push(delay.to_string());
+ }
+
+ if let Some(attempts) = self.0.restart_attempts {
+ args.push("-a".to_string());
+ args.push(attempts.to_string());
+ }
+
+ if let Some(policy) = self.0.restart_policy {
+ args.push("-P".to_string());
+ args.push(policy.to_string());
+ }
+
+ if let Some(pwd) = self.0.pwd {
+ args.push("-p".to_string());
+ args.push(pwd);
+ }
+
+ if let Some(root) = self.0.root {
+ args.push("-r".to_string());
+ args.push(root);
+ }
+
+ for pair in self.0.env {
+ args.push("-e".to_string());
+ args.push(pair);
+ }
+
+ if let Some(group) = self.0.group {
+ args.push("-g".to_string());
+ args.push(group);
+ }
+
+ if let Some(user) = self.0.user {
+ args.push("-u".to_string());
+ args.push(user);
+ }
+
+ if let Some(stdout) = self.0.stdout {
+ args.push("--stdout".to_string());
+ args.push(stdout);
+ }
+
+ if let Some(stderr) = self.0.stderr {
+ args.push("--stderr".to_string());
+ args.push(stderr);
+ }
+
+ if let Some(cgroup) = self.0.cgroup {
+ args.push("--cgroup".to_string());
+ args.push(cgroup);
+ }
+
+ args.push("--".to_string());
+
+ args.push(self.0.exec);
+
+ args.extend_from_slice(&self.0.args);
+
+ Command::new("kanit-supervisor")
+ .args(args)
+ .spawn()
+ .context_kind("failed to spawn supervisor", ErrorKind::Recoverable)
+ }
+
+ pub fn restart_delay(mut self, delay: u64) -> Self {
+ self.0.restart_delay = Some(delay);
+ self
+ }
+
+ pub fn restart_attempts(mut self, attempts: u64) -> Self {
+ self.0.restart_attempts = Some(attempts);
+ self
+ }
+
+ pub fn restart_policy(mut self, policy: RestartPolicy) -> Self {
+ self.0.restart_policy = Some(policy);
+ self
+ }
+
+ pub fn cgroup(mut self, cgroup: String) -> Self {
+ self.0.cgroup = Some(cgroup);
+ self
+ }
+
+ pub fn pwd(mut self, pwd: String) -> Self {
+ self.0.pwd = Some(pwd);
+ self
+ }
+
+ pub fn root(mut self, root: String) -> Self {
+ self.0.root = Some(root);
+ self
+ }
+
+ pub fn env(mut self, key: String, value: String) -> Self {
+ self.0.env.push(format!("{key}={value}"));
+ self
+ }
+
+ pub fn group(mut self, group: String) -> Self {
+ self.0.group = Some(group);
+ self
+ }
+
+ pub fn user(mut self, user: String) -> Self {
+ self.0.user = Some(user);
+ self
+ }
+
+ pub fn stdout(mut self, stdout: String) -> Self {
+ self.0.stdout = Some(stdout);
+ self
+ }
+
+ pub fn stderr(mut self, stderr: String) -> Self {
+ self.0.stderr = Some(stderr);
+ self
+ }
+}
diff --git a/crates/supervisor/src/cli.rs b/crates/supervisor/src/cli.rs
@@ -0,0 +1,124 @@
+use std::fs::{read_to_string, write};
+use std::os::unix::prelude::ExitStatusExt;
+use std::path::Path;
+use std::process;
+use std::process::{ExitCode, ExitStatus};
+
+use nix::errno::Errno;
+use nix::libc::pid_t;
+use nix::sched::{CloneFlags, unshare};
+use nix::sys::signal;
+use nix::sys::signal::{Signal, kill};
+use nix::sys::signalfd::{SfdFlags, SigSet, SignalFd};
+use nix::sys::stat::Mode;
+use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
+use nix::unistd::{Pid, mkdir};
+
+use kanit_common::error::{Context, Result};
+
+use crate::{Supervisor, spawn, spawn_restart};
+
+fn cgroup_signal(name: &str, signal: Signal) -> Result<()> {
+ let procs_path = Path::new("/sys/fs/cgroup/system.slice")
+ .join(name)
+ .join("cgroup.procs");
+
+ read_to_string(procs_path)
+ .context("failed to read procs")?
+ .split('\n')
+ .filter_map(|pid| pid.parse::<u32>().ok())
+ .try_for_each(|pid| {
+ if pid == process::id() {
+ return Ok(());
+ }
+
+ let _ = kill(Pid::from_raw(pid as pid_t), signal);
+
+ Ok(())
+ })
+}
+
+pub fn handle_cli() -> ExitCode {
+ match Supervisor::from_env() {
+ Ok(mut cfg) => {
+ let mut mask = SigSet::empty();
+ mask.add(signal::SIGCHLD);
+ mask.add(signal::SIGTERM);
+ mask.thread_block().unwrap();
+
+ let sfd = SignalFd::with_flags(&mask, SfdFlags::empty()).unwrap();
+
+ if let Some(ref cgroup) = cfg.cgroup {
+ unshare(CloneFlags::CLONE_NEWCGROUP).expect("unshare cgroup");
+
+ let supervisor_cgroup = Path::new("/sys/fs/cgroup/system.slice").join(cgroup);
+
+ mkdir(
+ &supervisor_cgroup,
+ Mode::S_IRWXU | Mode::S_IRGRP | Mode::S_IRUSR,
+ )
+ .expect("create directory");
+
+ write(
+ supervisor_cgroup.join("cgroup.procs"),
+ process::id().to_string(),
+ )
+ .expect("write cgroup proc");
+ }
+
+ #[allow(clippy::zombie_processes)] // we use `waitpid` and not `.wait`
+ let mut child = spawn(&cfg).expect("spawn child");
+
+ loop {
+ match sfd.read_signal() {
+ Ok(Some(sig)) => match Signal::try_from(sig.ssi_signo as i32) {
+ Ok(Signal::SIGCHLD) => {
+ loop {
+ let pid = waitpid(None, Some(WaitPidFlag::WNOHANG));
+
+ match pid {
+ Ok(WaitStatus::StillAlive) => break,
+ Err(Errno::ECHILD) => break,
+ Err(e) => {
+ eprintln!("failed to waitpid: {e}");
+ return ExitCode::FAILURE;
+ }
+ _ => {}
+ }
+ }
+
+ if let Some(c) =
+ spawn_restart(&mut cfg, ExitStatus::from_raw(sig.ssi_status), true)
+ .expect("restart child")
+ {
+ child = c;
+ } else {
+ return ExitCode::SUCCESS;
+ }
+ }
+ Ok(Signal::SIGTERM) => {
+ child.kill().expect("kill child");
+ if let Some(attempts) = cfg.restart_attempts {
+ cfg.restart_attempts = Some(attempts + 1);
+ }
+ if let Some(ref cgroup) = cfg.cgroup {
+ cgroup_signal(cgroup.as_ref(), Signal::SIGKILL)
+ .expect("kill child");
+ }
+ }
+ _ => {}
+ },
+ Ok(None) => unreachable!(),
+ Err(e) => {
+ eprintln!("{e}");
+ return ExitCode::FAILURE;
+ }
+ }
+ }
+ }
+ Err(e) => {
+ eprintln!("{e}");
+ ExitCode::FAILURE
+ }
+ }
+}
diff --git a/crates/supervisor/src/flags.rs b/crates/supervisor/src/flags.rs
@@ -0,0 +1,127 @@
+use std::fmt::Formatter;
+use std::str::FromStr;
+use std::{error, fmt};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RestartPolicy {
+ Never,
+ Always,
+ OnSuccess,
+ OnFailure,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ParseRestartModeError;
+
+impl fmt::Display for ParseRestartModeError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "expected `never`, `always`, `on-success`, or `on-failure`"
+ )
+ }
+}
+
+impl error::Error for ParseRestartModeError {}
+
+impl FromStr for RestartPolicy {
+ type Err = ParseRestartModeError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "never" => Ok(Self::Never),
+ "always" => Ok(Self::Always),
+ "on-success" => Ok(Self::OnSuccess),
+ "on-failure" => Ok(Self::OnFailure),
+ _ => Err(ParseRestartModeError),
+ }
+ }
+}
+
+impl fmt::Display for RestartPolicy {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Never => write!(f, "never"),
+ Self::Always => write!(f, "always"),
+ Self::OnSuccess => write!(f, "on-success"),
+ Self::OnFailure => write!(f, "on-failure"),
+ }
+ }
+}
+
+#[cfg(feature = "cli")]
+xflags::xflags! {
+ src "./src/flags.rs"
+
+ cmd supervisor {
+ /// Delay used before restarting an exited process.
+ optional -d, --restart-delay delay: u64
+ /// Amount of times to restart a process before giving up.
+ optional -a, --restart-attempts attempts: u64
+ /// Policy to use before restarting, either `never`, `always`, `on-success`, or `on-failure`.
+ optional -P, --restart-policy policy: RestartPolicy
+ /// Set the working directory.
+ optional -p, --pwd pwd: String
+ /// Set the root directory.
+ optional -r, --root root: String
+ /// Set an environment variable (NAME=VAR).
+ repeated -e, --env pair: String
+ /// Set the process group.
+ optional -g, --group gid: String
+ /// Set the process user.
+ optional -u, --user uid: String
+ /// Redirect stdout to path.
+ optional --stdout path: String
+ /// Redirect stderr to path.
+ optional --stderr path: String
+ /// Cgroup to create.
+ optional --cgroup cgroup: String
+ /// Command to execute.
+ required exec: String
+ /// Arguments passed to the command.
+ repeated args: String
+ }
+}
+
+#[derive(Clone, Default)]
+// TODO; paths should be paths
+// impl needs to be marked `#[cfg(feature = "cli")]`
+// generated start
+// The following code is generated by `xflags` macro.
+// Run `env UPDATE_XFLAGS=1 cargo build` to regenerate.
+#[derive(Debug)]
+pub struct Supervisor {
+ pub exec: String,
+ pub args: Vec<String>,
+
+ pub restart_delay: Option<u64>,
+ pub restart_attempts: Option<u64>,
+ pub restart_policy: Option<RestartPolicy>,
+ pub cgroup: Option<String>,
+ pub pwd: Option<String>,
+ pub root: Option<String>,
+ pub env: Vec<String>,
+ pub group: Option<String>,
+ pub user: Option<String>,
+ pub stdout: Option<String>,
+ pub stderr: Option<String>,
+}
+
+#[cfg(feature = "cli")]
+impl Supervisor {
+ #[allow(dead_code)]
+ pub fn from_env_or_exit() -> Self {
+ Self::from_env_or_exit_()
+ }
+
+ #[allow(dead_code)]
+ pub fn from_env() -> xflags::Result<Self> {
+ Self::from_env_()
+ }
+
+ #[allow(dead_code)]
+ pub fn from_vec(args: Vec<std::ffi::OsString>) -> xflags::Result<Self> {
+ Self::from_vec_(args)
+ }
+}
+// generated end
diff --git a/crates/supervisor/src/lib.rs b/crates/supervisor/src/lib.rs
@@ -0,0 +1,11 @@
+pub use builder::*;
+#[cfg(feature = "cli")]
+pub use cli::handle_cli;
+pub use flags::*;
+pub use supervisor::*;
+
+mod builder;
+#[cfg(feature = "cli")]
+mod cli;
+mod flags;
+mod supervisor;
diff --git a/crates/supervisor/src/main.rs b/crates/supervisor/src/main.rs
@@ -0,0 +1,12 @@
+use std::process::ExitCode;
+
+#[cfg(feature = "cli")]
+fn main() -> ExitCode {
+ kanit_supervisor::handle_cli()
+}
+
+#[cfg(not(feature = "cli"))]
+fn main() -> ExitCode {
+ eprintln!("supervisor compiled without command line");
+ ExitCode::FAILURE
+}
diff --git a/crates/supervisor/src/supervisor.rs b/crates/supervisor/src/supervisor.rs
@@ -0,0 +1,115 @@
+use std::fs::OpenOptions;
+use std::os::unix::fs::chroot;
+use std::os::unix::process::CommandExt;
+use std::process::{Child, Command, ExitStatus, Stdio};
+use std::thread::sleep;
+use std::time::Duration;
+
+use nix::unistd::{Group, User};
+
+use kanit_common::error::{Context, Result};
+
+use crate::flags::{RestartPolicy, Supervisor};
+
+pub fn spawn_restart(
+ cfg: &mut Supervisor,
+ status: ExitStatus,
+ delay: bool,
+) -> Result<Option<Child>> {
+ match cfg.restart_policy.unwrap_or(RestartPolicy::Never) {
+ RestartPolicy::Never => return Ok(None),
+ RestartPolicy::OnFailure if status.success() => return Ok(None),
+ RestartPolicy::OnSuccess if !status.success() => return Ok(None),
+ _ => {}
+ }
+
+ if let Some(attempts) = cfg.restart_attempts {
+ if attempts == 0 {
+ return Ok(None);
+ } else {
+ cfg.restart_attempts = Some(attempts - 1);
+ }
+ }
+
+ if let Some(delay_sec) = cfg.restart_delay {
+ if delay {
+ sleep(Duration::from_secs(delay_sec))
+ }
+ }
+
+ spawn(cfg).map(Some)
+}
+
+pub fn spawn(cfg: &Supervisor) -> Result<Child> {
+ let mut cmd = Command::new(&cfg.exec);
+
+ cmd.args(&cfg.args);
+ cmd.envs(cfg.env.iter().filter_map(|pair| pair.split_once('=')));
+
+ if let Some(ref dir) = cfg.pwd {
+ cmd.current_dir(dir);
+ }
+
+ if let Some(ref dir) = cfg.root {
+ let dir = dir.clone();
+
+ // SAFETY: we only call async-signal-safe functions (chroot)
+ unsafe {
+ cmd.pre_exec(move || chroot(&dir));
+ }
+ }
+
+ if let Some(ref user) = cfg.user {
+ let uid = user
+ .parse::<u32>()
+ .map(Some)
+ .unwrap_or_else(|_| {
+ User::from_name(user)
+ .map(|u| u.map(|u| u.uid.as_raw()))
+ .unwrap_or(None)
+ })
+ .context("failed to parse/locate user")?;
+
+ cmd.uid(uid);
+ }
+
+ if let Some(ref group) = cfg.group {
+ let gid = group
+ .parse::<u32>()
+ .map(Some)
+ .unwrap_or_else(|_| {
+ Group::from_name(group)
+ .map(|g| g.map(|g| g.gid.as_raw()))
+ .unwrap_or(None)
+ })
+ .context("failed to parse/locate group")?;
+
+ cmd.gid(gid);
+ }
+
+ if let Some(ref stdout) = cfg.stdout {
+ let f = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(stdout)
+ .context("failed to open stdout")?;
+
+ cmd.stdout(f);
+ } else {
+ cmd.stdout(Stdio::null());
+ }
+
+ if let Some(ref stderr) = cfg.stderr {
+ let f = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(stderr)
+ .context("failed to open stderr")?;
+
+ cmd.stderr(f);
+ } else {
+ cmd.stderr(Stdio::null());
+ }
+
+ cmd.spawn().context("failed to spawn child")
+}
diff --git a/crates/unit/Cargo.toml b/crates/unit/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "kanit-unit"
+version.workspace = true
+edition.workspace = true
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.base64]
+version = "0.22"
+
+[dependencies.nix]
+version = "0.29"
+features = ["sched", "signal"]
+
+[dependencies.blocking]
+version = "1.6"
+
+[dependencies.nom]
+version = "7.1"
+
+[dependencies.async-trait]
+version = "0.1"
+
+[dependencies.async-fs]
+version = "2.1"
+
+[dependencies.send_wrapper]
+version = "0.6"
+
+[dependencies.kanit-supervisor]
+path = "../supervisor"
+
+[dependencies.kanit-common]
+path = "../common"
diff --git a/crates/unit/src/dependencies.rs b/crates/unit/src/dependencies.rs
@@ -0,0 +1,54 @@
+// A needs B | A -> B
+// A uses B | <ignored>
+// A wants B | A -> B (if impossible tree, it will be discarded)
+// A before B | B -> A
+// A after B | A -> B
+
+use crate::UnitName;
+
+#[derive(Default, Debug, Clone)]
+pub struct Dependencies {
+ /// The unit requires a previous unit to be started before it.
+ /// The unit will fail to start if any of its needs fail to start.
+ pub needs: Vec<UnitName>,
+ /// The unit uses another unit but doesn't require it.
+ /// The used unit will not be started, however.
+ pub uses: Vec<UnitName>,
+ /// Similar to `uses` with the exception that wanted unit will be started.
+ pub wants: Vec<UnitName>,
+ /// The unit should run before another unit.
+ pub before: Vec<UnitName>,
+ /// The unit should run after another unit.
+ pub after: Vec<UnitName>,
+}
+
+impl Dependencies {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn need(&mut self, dependency: UnitName) -> &mut Self {
+ self.needs.push(dependency);
+ self
+ }
+
+ pub fn uses(&mut self, dependency: UnitName) -> &mut Self {
+ self.uses.push(dependency);
+ self
+ }
+
+ pub fn want(&mut self, dependency: UnitName) -> &mut Self {
+ self.wants.push(dependency);
+ self
+ }
+
+ pub fn before(&mut self, dependency: UnitName) -> &mut Self {
+ self.before.push(dependency);
+ self
+ }
+
+ pub fn after(&mut self, dependency: UnitName) -> &mut Self {
+ self.after.push(dependency);
+ self
+ }
+}
diff --git a/crates/unit/src/formats/config.rs b/crates/unit/src/formats/config.rs
@@ -0,0 +1,201 @@
+// generic config handling
+// # comment
+// [Header]
+// key = [a-zA-Z_][a-zA-Z_-0-9]*
+
+use std::collections::HashMap;
+
+use nom::IResult;
+use nom::branch::alt;
+use nom::bytes::complete::{is_a, tag};
+use nom::character::complete::{
+ alpha1, alphanumeric1, char, multispace0, multispace1, not_line_ending,
+};
+use nom::combinator::{all_consuming, map, recognize, value};
+use nom::multi::many0;
+use nom::sequence::{delimited, pair, separated_pair};
+
+pub fn config_file(input: &str) -> IResult<&str, HashMap<&str, HashMap<&str, &str>>> {
+ all_consuming(map(pair(body, many0(category_body)), |(root, mut rest)| {
+ rest.push((".root", root));
+ rest.into_iter().collect()
+ }))(input)
+}
+
+fn category_body(input: &str) -> IResult<&str, (&str, HashMap<&str, &str>)> {
+ pair(category, body)(input)
+}
+
+fn category(input: &str) -> IResult<&str, &str> {
+ delimited(char('['), identifier, char(']'))(input)
+}
+
+fn body(input: &str) -> IResult<&str, HashMap<&str, &str>> {
+ map(many0(delimited(ignored, key_value, ignored)), |map| {
+ map.into_iter().collect()
+ })(input)
+}
+
+fn ignored(input: &str) -> IResult<&str, ()> {
+ value((), many0(alt((comment, value((), multispace1)))))(input)
+}
+
+fn comment(input: &str) -> IResult<&str, ()> {
+ value((), pair(char('#'), not_line_ending))(input)
+}
+
+fn key_value(input: &str) -> IResult<&str, (&str, &str)> {
+ separated_pair(
+ identifier,
+ delimited(multispace0, char('='), multispace0),
+ not_line_ending,
+ )(input)
+}
+
+fn identifier(input: &str) -> IResult<&str, &str> {
+ recognize(pair(
+ alt((alpha1, tag("_"))),
+ many0(alt((alphanumeric1, is_a("-_")))),
+ ))(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use nom::error::Error;
+
+ use crate::parser_test;
+
+ use super::*;
+
+ #[test]
+ fn parse_identifier() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ identifier,
+ [
+ "foo" => "foo",
+ "foo2" => "foo2",
+ "foo_bar" => "foo_bar",
+ "foo-bar" => "foo-bar"
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_key_value() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ key_value,
+ [
+ "foo=bar" => ("foo", "bar"),
+ "foo = bar" => ("foo", "bar")
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_comment() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ comment,
+ [
+ "#foo" => (),
+ "# foo" => ()
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_ignored() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ ignored,
+ [
+ "#foo" => (),
+ "# foo" => (),
+ " " => (),
+ " \t \n" => ()
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_body() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ body,
+ [
+ "x = y\ny=z" => HashMap::from([
+ ("x", "y"),
+ ("y", "z")
+ ]),
+ "x = y\n# foo\n\ny=z" => HashMap::from([
+ ("x", "y"),
+ ("y", "z")
+ ])
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_category() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ category,
+ [
+ "[foo]" => "foo",
+ "[foo-bar]" => "foo-bar"
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_category_body() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ category_body,
+ [
+ "[foo]\nx = y\n# foo\ny=z" => (
+ "foo",
+ HashMap::from([
+ ("x", "y"),
+ ("y", "z")
+ ])
+ )
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_config_file() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ config_file,
+ [
+ "# foo\n\nx = y\n[foo]\nx = y\n# foo\ny=z" => HashMap::from([
+ (
+ ".root",
+ HashMap::from([
+ ("x", "y")
+ ])
+ ),
+ (
+ "foo",
+ HashMap::from([
+ ("x", "y"),
+ ("y", "z")
+ ])
+ )
+ ])
+ ]
+ );
+
+ Ok(())
+ }
+}
diff --git a/crates/unit/src/formats/grouping.rs b/crates/unit/src/formats/grouping.rs
@@ -0,0 +1,280 @@
+// grouping
+
+// @<mtime>
+// :sysboot
+// procfs,modules,devfs
+// run,clock,sysfs
+// hostname,hwdrivers,mdev,rootfs
+// swap
+// localmount
+// syslog,seed
+// :boot
+// :default
+// getty@tty1
+
+use std::collections::HashMap;
+use std::fmt;
+use std::fmt::Formatter;
+use std::str::FromStr;
+
+use base64::Engine;
+use base64::prelude::BASE64_STANDARD;
+use nom::branch::alt;
+use nom::bytes::complete::is_a;
+use nom::character::complete::{alpha1, alphanumeric1, char, newline, not_line_ending};
+use nom::combinator::{all_consuming, map, map_res, opt, recognize};
+use nom::error::Error;
+use nom::multi::{many0, separated_list0, separated_list1};
+use nom::sequence::{delimited, pair, preceded, terminated};
+use nom::{Finish, IResult};
+
+use crate::UnitName;
+
+type Sha256Hash = [u8; 32];
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct DependencyGrouping {
+ pub hash: Sha256Hash,
+ pub groups: HashMap<Box<str>, Vec<Vec<UnitName>>>,
+}
+
+impl fmt::Display for DependencyGrouping {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ writeln!(f, "@{}", BASE64_STANDARD.encode(self.hash))?;
+
+ for (name, levels) in self.groups.iter() {
+ writeln!(f, ":{name}")?;
+
+ for level in levels.iter() {
+ writeln!(f, "{}", level.join(","))?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl FromStr for DependencyGrouping {
+ type Err = Error<String>;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match grouping(s).finish() {
+ Ok((_remaining, grouping)) => Ok(grouping),
+ Err(e) => Err(Error {
+ input: e.input.to_string(),
+ code: e.code,
+ }),
+ }
+ }
+}
+
+fn grouping(input: &str) -> IResult<&str, DependencyGrouping> {
+ all_consuming(map(
+ pair(header, separated_list1(newline, group)),
+ |(hash, groups)| DependencyGrouping {
+ hash,
+ groups: groups.into_iter().collect(),
+ },
+ ))(input)
+}
+
+fn header(input: &str) -> IResult<&str, Sha256Hash> {
+ delimited(char('@'), map_res(not_line_ending, hash), newline)(input)
+}
+
+fn hash(input: &str) -> Result<Sha256Hash, base64::DecodeSliceError> {
+ let mut buff: Sha256Hash = [0; 32];
+
+ BASE64_STANDARD.decode_slice(input, &mut buff)?;
+
+ Ok(buff)
+}
+
+fn group(input: &str) -> IResult<&str, (Box<str>, Vec<Vec<UnitName>>)> {
+ pair(
+ terminated(group_name, opt(newline)),
+ separated_list0(newline, level),
+ )(input)
+}
+
+fn group_name(input: &str) -> IResult<&str, Box<str>> {
+ preceded(
+ char(':'),
+ map(
+ recognize(pair(alpha1, many0(alt((alphanumeric1, is_a("-_")))))),
+ Box::from,
+ ),
+ )(input)
+}
+
+fn level(input: &str) -> IResult<&str, Vec<UnitName>> {
+ separated_list1(char(','), unit_name)(input)
+}
+
+fn unit_name(input: &str) -> IResult<&str, UnitName> {
+ map(
+ recognize(pair(alpha1, many0(alt((alphanumeric1, is_a("-_.@")))))),
+ UnitName::from,
+ )(input)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::parser_test;
+
+ use super::*;
+
+ // TODO; we should have negative tests
+ #[test]
+ fn parse_unit_name() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ unit_name,
+ [
+ "foo" => UnitName::from("foo"),
+ "foo2" => UnitName::from("foo2"),
+ "foo_bar" => UnitName::from("foo_bar"),
+ "foo-bar" => UnitName::from("foo-bar"),
+ "foo@bar" => UnitName::from("foo@bar")
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_level() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ level,
+ [
+ "foo,bar,baz" => vec![
+ UnitName::from("foo"),
+ UnitName::from("bar"),
+ UnitName::from("baz"),
+ ],
+ "foo" => vec![
+ UnitName::from("foo")
+ ]
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_group_name() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ group_name,
+ [
+ ":foo" => Box::from("foo"),
+ ":foo2" => Box::from("foo2"),
+ ":foo_bar" => Box::from("foo_bar"),
+ ":foo-bar" => Box::from("foo-bar")
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_group() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ group,
+ [
+ ":foo" => (Box::from("foo"), vec![]),
+ ":foo\nbar,baz" => (
+ Box::from("foo"),
+ vec![vec![UnitName::from("bar"), UnitName::from("baz")]]
+ )
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_header() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ header,
+ [
+ "@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n" => [0; 32],
+ "@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\n" => [1; 32]
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_grouping() -> Result<(), Error<&'static str>> {
+ parser_test!(
+ grouping,
+ [
+ "@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n:sysboot\nprocfs,modules\nrun" => DependencyGrouping {
+ hash: [0; 32],
+ groups: HashMap::from([
+ (
+ Box::from("sysboot"),
+ vec![
+ vec![UnitName::from("procfs"), UnitName::from("modules")],
+ vec![UnitName::from("run")]
+ ]
+ )
+ ])
+ },
+ "@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\n:sysboot\nprocfs,modules\nrun\n:default\ngetty@tty1" => DependencyGrouping {
+ hash: [1; 32],
+ groups: HashMap::from([
+ (
+ Box::from("sysboot"),
+ vec![
+ vec![UnitName::from("procfs"), UnitName::from("modules")],
+ vec![UnitName::from("run")]
+ ]
+ ),
+ (
+ Box::from("default"),
+ vec![
+ vec![UnitName::from("getty@tty1")]
+ ]
+ )
+ ])
+ }
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn display_grouping() {
+ let grouping = DependencyGrouping {
+ hash: [2; 32],
+ groups: HashMap::from([
+ (
+ Box::from("sysboot"),
+ vec![
+ vec![UnitName::from("procfs"), UnitName::from("modules")],
+ vec![UnitName::from("run")],
+ ],
+ ),
+ (
+ Box::from("default"),
+ vec![
+ vec![UnitName::from("syslog")],
+ vec![UnitName::from("getty@tty1")],
+ ],
+ ),
+ ]),
+ };
+
+ // hashmap ordering leads to 2 possible orderings
+ let output = grouping.to_string();
+
+ match output.as_str() {
+ "@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\n:default\nsyslog\ngetty@tty1\n:sysboot\nprocfs,modules\nrun\n"
+ | "@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\n:sysboot\nprocfs,modules\nrun\n:default\nsyslog\ngetty@tty1\n" =>
+ {}
+ s => panic!("did not expect {s}"),
+ }
+ }
+}
diff --git a/crates/unit/src/formats/mod.rs b/crates/unit/src/formats/mod.rs
@@ -0,0 +1,8 @@
+pub use config::config_file;
+pub use grouping::DependencyGrouping;
+pub use unit::{Unit, UnitKind};
+
+mod config;
+mod grouping;
+mod testing;
+mod unit;
diff --git a/crates/unit/src/formats/testing.rs b/crates/unit/src/formats/testing.rs
@@ -0,0 +1,25 @@
+#[cfg(test)]
+#[macro_export]
+macro_rules! assert_parser {
+ ($e:expr $(,)?) => {
+ assert_parser!($e, "parser did not complete")
+ };
+ ($e:expr, $($arg:tt)+) => {
+ if let ::std::result::Result::Ok((i, r)) = $e {
+ assert!(i.is_empty(), "did not read EOF, remaining `{i}`");
+ r
+ } else {
+ panic!($($arg)+)
+ }
+ };
+}
+
+#[cfg(test)]
+#[macro_export]
+macro_rules! parser_test {
+ ($p:expr, [ $( $i:literal => $e:expr ),* ]) => {
+ $(
+ assert_eq!($crate::assert_parser!($p($i), "failed with `{}`", $i), $e);
+ )*
+ }
+}
diff --git a/crates/unit/src/formats/unit.rs b/crates/unit/src/formats/unit.rs
@@ -0,0 +1,397 @@
+// rewrite cmd => exec, args
+
+use std::collections::HashMap;
+use std::fmt::Formatter;
+use std::path::Path;
+use std::str::FromStr;
+use std::{error, fmt};
+
+use async_trait::async_trait;
+use blocking::unblock;
+use log::warn;
+use nix::libc::pid_t;
+use nix::sys::signal::{Signal, kill};
+use nix::unistd::Pid;
+use nom::branch::alt;
+use nom::bytes::complete::{is_a, take_while, take_while1};
+use nom::character::complete::{char, multispace0, multispace1};
+use nom::combinator::{all_consuming, cut, map, map_res, value, verify};
+use nom::error::Error;
+use nom::multi::{fold_many0, separated_list0};
+use nom::sequence::{preceded, separated_pair, terminated};
+use nom::{Finish, IResult};
+
+use kanit_common::constants;
+use kanit_common::error::{Context, Result};
+use kanit_supervisor::{RestartPolicy, Supervisor, SupervisorBuilder};
+
+use crate::formats::config_file;
+use crate::{Dependencies, UnitName};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum UnitKind {
+ Oneshot,
+ Daemon,
+ Builtin,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ParseUnitKindError;
+
+impl fmt::Display for ParseUnitKindError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "expected `oneshot`, `daemon`, or `builtin`")
+ }
+}
+
+impl error::Error for ParseUnitKindError {}
+
+impl FromStr for UnitKind {
+ type Err = ParseUnitKindError;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ match s {
+ "oneshot" => Ok(Self::Oneshot),
+ "daemon" => Ok(Self::Daemon),
+ "builtin" => Ok(Self::Builtin),
+ _ => Err(ParseUnitKindError),
+ }
+ }
+}
+
+fn unit_name_vec(val: Option<&&str>) -> Vec<UnitName> {
+ val.map(|s| s.split(',').map(UnitName::from).collect())
+ .unwrap_or_default()
+}
+
+impl Dependencies {
+ fn from_body(body: &HashMap<&str, &str>) -> Self {
+ Self {
+ before: unit_name_vec(body.get("before")),
+ after: unit_name_vec(body.get("after")),
+ needs: unit_name_vec(body.get("needs")),
+ uses: unit_name_vec(body.get("uses")),
+ wants: unit_name_vec(body.get("wants")),
+ }
+ }
+}
+
+// this is stupid but it works so it isn't stupid
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) struct Command(String, Vec<String>);
+
+impl FromStr for Command {
+ type Err = Error<String>;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ match parse_cmd(s).finish() {
+ Ok((_remaining, cmd)) => Ok(cmd),
+ Err(e) => Err(Error {
+ input: e.input.to_string(),
+ code: e.code,
+ }),
+ }
+ }
+}
+
+// could this be lowered?
+// to be fair, using supervisor with a shell allows you to get all the benefits
+// the shell will automatically do splitting
+fn parse_cmd(input: &str) -> IResult<&str, Command> {
+ all_consuming(map(separated_pair(command, multispace0, args), |(c, a)| {
+ Command(c, a)
+ }))(input)
+}
+
+fn command(input: &str) -> IResult<&str, String> {
+ map(take_while1(|c| c != ' '), String::from)(input)
+}
+
+fn args(input: &str) -> IResult<&str, Vec<String>> {
+ separated_list0(
+ multispace1,
+ alt((quoted_arg, map(take_while1(|c| c != ' '), String::from))),
+ )(input)
+}
+
+enum Fragment<'a> {
+ Char(char),
+ Str(&'a str),
+}
+
+fn quoted_arg(input: &str) -> IResult<&str, String> {
+ let (input, open_quote) = is_a("'\"")(input)?;
+ let quote_char = open_quote.chars().next().unwrap();
+
+ let inner_str = fold_many0(
+ alt((
+ map(
+ verify(
+ take_while(move |c| c != '\\' && c != quote_char),
+ |s: &str| !s.is_empty(),
+ ),
+ Fragment::Str,
+ ),
+ map(
+ preceded(
+ char('\\'),
+ alt((
+ value('\n', char('n')),
+ value('\r', char('r')),
+ value('\t', char('t')),
+ value('\\', char('\\')),
+ value(quote_char, char(quote_char)),
+ )),
+ ),
+ Fragment::Char,
+ ),
+ )),
+ String::new,
+ |mut buff, fragment| {
+ match fragment {
+ Fragment::Char(c) => buff.push(c),
+ Fragment::Str(s) => buff.push_str(s),
+ }
+ buff
+ },
+ );
+
+ cut(terminated(inner_str, char(quote_char)))(input)
+}
+
+#[derive(Debug, Clone)]
+pub struct Unit {
+ pub kind: UnitKind,
+ pub description: Option<Box<str>>,
+ pub dependencies: Option<Dependencies>,
+ pub supervisor: Option<Supervisor>,
+}
+
+fn parse_unit(input: &str) -> IResult<&str, Unit> {
+ map_res(config_file, Unit::from_config)(input)
+}
+
+impl FromStr for Unit {
+ type Err = Error<String>;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ match parse_unit(s).finish() {
+ Ok((_remaining, unit)) => Ok(unit),
+ Err(e) => Err(Error {
+ input: e.input.to_string(),
+ code: e.code,
+ }),
+ }
+ }
+}
+
+impl Unit {
+ pub fn from_config(config: HashMap<&str, HashMap<&str, &str>>) -> Result<Self> {
+ let root: HashMap<String, String> = config
+ .get(".root")
+ .map(|n| {
+ n.iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect()
+ })
+ .context("expected root node")?; // should be impossible
+ let kind = root
+ .get("kind")
+ .context("expected kind")?
+ .parse::<UnitKind>()
+ .context("failed to parse unit kind")?;
+ let description = root.get("description").map(|n| Box::from(n.as_str()));
+ let dependencies = config.get("depends").map(Dependencies::from_body);
+ let supervisor = if let UnitKind::Daemon | UnitKind::Oneshot = kind {
+ let (restart_delay, restart_attempts, restart_policy) =
+ if let Some(restart) = config.get("restart") {
+ let delay = if let Some(delay) = restart.get("delay") {
+ Some(delay.parse::<u64>().context("failed to parse delay")?)
+ } else {
+ None
+ };
+ let attempts = if let Some(attempts) = restart.get("attempts") {
+ Some(
+ attempts
+ .parse::<u64>()
+ .context("failed to parse attempts")?,
+ )
+ } else {
+ None
+ };
+ let policy = if let Some(policy) = restart.get("policy") {
+ Some(
+ policy
+ .parse::<RestartPolicy>()
+ .context("failed to parse policy")?,
+ )
+ } else {
+ Some(RestartPolicy::OnFailure)
+ };
+ (delay, attempts, policy)
+ } else {
+ (None, None, Some(RestartPolicy::OnFailure))
+ };
+
+ let pwd = root.get("pwd").map(|n| n.to_string());
+ let root_dir = root.get("root").map(|n| n.to_string());
+ let group = root.get("group").map(|n| n.to_string());
+ let user = root.get("user").map(|n| n.to_string());
+ let stdout = root.get("stdout").map(|n| n.to_string());
+ let stderr = root.get("stderr").map(|n| n.to_string());
+
+ let cmd = root
+ .get("cmd")
+ .context("expected cmd")?
+ .parse::<Command>()
+ .context("failed to parse cmd")?;
+
+ let env = config
+ .get("environment")
+ .map(|n| n.iter().map(|(k, v)| format!("{k}={v}")).collect())
+ .unwrap_or_default();
+
+ Some(Supervisor {
+ exec: cmd.0,
+ args: cmd.1,
+ cgroup: None,
+ restart_delay,
+ restart_attempts,
+ restart_policy,
+ pwd,
+ root: root_dir,
+ env,
+ group,
+ user,
+ stdout,
+ stderr,
+ })
+ } else {
+ None
+ };
+
+ Ok(Unit {
+ kind,
+ description,
+ dependencies,
+ supervisor,
+ })
+ }
+}
+
+#[async_trait]
+impl crate::Unit for (UnitName, Unit) {
+ fn name(&self) -> UnitName {
+ self.0.clone()
+ }
+
+ fn description(&self) -> Option<&str> {
+ self.1.description.as_deref()
+ }
+
+ fn dependencies(&self) -> Dependencies {
+ self.1.dependencies.clone().unwrap_or_default()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ if let Some(mut supervisor) = self.1.supervisor.clone() {
+ supervisor.cgroup = Some(self.0.to_string());
+
+ let child = SupervisorBuilder::from_supervisor(supervisor).spawn()?;
+
+ async_fs::write(
+ Path::new(constants::KAN_PIDS).join(self.0.to_string()),
+ child.id().to_string(),
+ )
+ .await
+ .context("failed to write pid")?;
+ }
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ if self.1.supervisor.is_none() {
+ return Ok(());
+ }
+
+ if let Ok(pid) =
+ async_fs::read_to_string(Path::new(constants::KAN_PIDS).join(self.0.to_string())).await
+ {
+ unblock(move || {
+ kill(
+ Pid::from_raw(pid.parse::<u32>().context("failed to parse pid")? as pid_t),
+ Signal::SIGTERM,
+ )
+ .context("failed to kill service")
+ })
+ .await?;
+ } else {
+ warn!("failed to find pid file");
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::parser_test;
+
+ use super::*;
+
+ #[test]
+ fn parse_unit_kind() {
+ assert_eq!("oneshot".parse::<UnitKind>().unwrap(), UnitKind::Oneshot);
+ assert_eq!("daemon".parse::<UnitKind>().unwrap(), UnitKind::Daemon);
+ assert_eq!("builtin".parse::<UnitKind>().unwrap(), UnitKind::Builtin);
+ assert_eq!("foo".parse::<UnitKind>(), Err(ParseUnitKindError));
+ }
+
+ #[test]
+ fn parse_command() -> std::result::Result<(), Error<&'static str>> {
+ parser_test!(
+ command,
+ [
+ "foo" => "foo",
+ "foo2" => "foo2"
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_args() -> std::result::Result<(), Error<&'static str>> {
+ parser_test!(
+ args,
+ [
+ "foo" => vec!["foo"],
+ "foo2 \"bar\"" => vec!["foo2", "bar"],
+ "\"\\\"baz\\\"\"" => vec!["\"baz\""],
+ "'\\n'" => vec!["\n"],
+ "'hello\\nworld'" => vec!["hello\nworld"]
+ ]
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_parse_cmd() -> std::result::Result<(), Error<&'static str>> {
+ parser_test!(
+ parse_cmd,
+ [
+ "foo" => Command(String::from("foo"), vec![]),
+ "foo bar" => Command(String::from("foo"), vec![String::from("bar")]),
+ "foo \"bar baz\"" => Command(String::from("foo"), vec![String::from("bar baz")]),
+ "foo \"bar baz\" \"\\n\"" => Command(
+ String::from("foo"),
+ vec![String::from("bar baz"), String::from("\n")]
+ )
+ ]
+ );
+
+ Ok(())
+ }
+}
diff --git a/crates/unit/src/lib.rs b/crates/unit/src/lib.rs
@@ -0,0 +1,6 @@
+pub use dependencies::*;
+pub use unit::*;
+
+mod dependencies;
+pub mod formats;
+mod unit;
diff --git a/crates/unit/src/unit.rs b/crates/unit/src/unit.rs
@@ -0,0 +1,75 @@
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use send_wrapper::SendWrapper;
+
+use kanit_common::error::Result;
+
+use crate::Dependencies;
+
+// has to be async lock to allow querying without possible blocks
+pub type RcUnit = SendWrapper<Rc<RefCell<dyn Unit>>>;
+pub type UnitName = Arc<str>;
+
+/// Unit lifecycle:
+///
+///
+/// Startup:
+/// ```rs
+/// if !unit.prepare().await? { return; }
+///
+/// unit.start().await?;
+/// ```
+///
+/// Stop:
+/// ```rs
+/// unit.stop().await?;
+/// unit.teardown().await?;
+/// ```
+///
+/// Restart:
+/// ```rs
+/// unit.stop().await?;
+/// unit.start().await?;
+/// ```
+#[async_trait]
+pub trait Unit: Send + Sync {
+ /// The name of the unit.
+ fn name(&self) -> UnitName;
+
+ /// A description of the unit.
+ fn description(&self) -> Option<&str> {
+ None
+ }
+
+ /// Dependencies of the unit.
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ }
+
+ /// Starts the unit.
+ async fn start(&mut self) -> Result<()> {
+ Ok(())
+ }
+
+ /// Called when a unit is ordered to stop or as a step in restarting.
+ async fn stop(&mut self) -> Result<()> {
+ Ok(())
+ }
+
+ /// Preconditions for starting a unit.
+ async fn prepare(&self) -> Result<bool> {
+ Ok(true)
+ }
+
+ /// Tearing down a unit once finished.
+ async fn teardown(&self) -> Result<()> {
+ Ok(())
+ }
+}
+
+pub fn wrap_unit<U: Unit + 'static>(unit: U) -> RcUnit {
+ SendWrapper::new(Rc::new(RefCell::new(unit)))
+}
diff --git a/crates/units/Cargo.toml b/crates/units/Cargo.toml
@@ -0,0 +1,58 @@
+[package]
+name = "kanit-units"
+version.workspace = true
+edition.workspace = true
+
+[features]
+testing = []
+
+[dependencies.async-process]
+version = "2.2"
+
+[dependencies.async-trait]
+version = "0.1"
+
+[dependencies.async-fs]
+version = "2.1"
+
+[dependencies.futures-lite]
+version = "2.3"
+
+[dependencies.blocking]
+version = "1.6"
+
+[dependencies.fastrand]
+version = "2.1"
+
+[dependencies.walkdir]
+version = "2.5"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.nix]
+version = "0.29"
+features = [
+ "signal",
+ "process",
+ "mount",
+ "hostname",
+ "time",
+ "ioctl",
+ "fs"
+]
+
+[dependencies.libc]
+version = "0.2"
+
+[dependencies.kanit-executor]
+path = "../executor"
+
+[dependencies.kanit-unit]
+path = "../unit"
+
+[dependencies.kanit-supervisor]
+path = "../supervisor"
+
+[dependencies.kanit-common]
+path = "../common"
diff --git a/crates/units/src/lib.rs b/crates/units/src/lib.rs
@@ -0,0 +1,6 @@
+pub use loader::{default_enabled, units};
+
+mod loader;
+mod mounts;
+pub mod oneshot;
+pub mod services;
diff --git a/crates/units/src/loader.rs b/crates/units/src/loader.rs
@@ -0,0 +1,52 @@
+use std::collections::{HashMap, HashSet};
+
+use kanit_unit::{RcUnit, UnitName, wrap_unit};
+
+use crate::oneshot::*;
+use crate::services::GeTTY;
+
+// use crate::services::*;
+
+pub fn units() -> [RcUnit; 16] {
+ [
+ wrap_unit(ProcFs),
+ wrap_unit(SysFs),
+ wrap_unit(Run),
+ wrap_unit(DevFs),
+ wrap_unit(MDev),
+ wrap_unit(HwDrivers),
+ wrap_unit(Modules),
+ wrap_unit(Clock),
+ wrap_unit(RootFs),
+ wrap_unit(Swap),
+ wrap_unit(LocalMount),
+ wrap_unit(Seed),
+ wrap_unit(Hostname),
+ wrap_unit(Cgroup),
+ // wrap_unit(Syslog::new()),
+ // TODO; getty kills init... as always
+ wrap_unit(GeTTY::new("tty1", false)),
+ wrap_unit(GeTTY::new("ttyS0", true)),
+ ]
+}
+
+pub fn default_enabled() -> HashMap<Box<str>, HashSet<UnitName>> {
+ let sysboot = units()
+ .iter()
+ .map(|n| n.borrow().name())
+ .filter(|n| !n.starts_with("getty"))
+ .collect::<HashSet<_>>();
+ #[cfg(not(feature = "testing"))]
+ let default = units()
+ .iter()
+ .map(|n| n.borrow().name())
+ .filter(|n| n.starts_with("getty"))
+ .collect::<HashSet<_>>();
+ #[cfg(feature = "testing")]
+ let default = HashSet::new();
+
+ HashMap::from([
+ (Box::from("sysboot"), sysboot),
+ (Box::from("default"), default),
+ ])
+}
diff --git a/crates/units/src/mounts.rs b/crates/units/src/mounts.rs
@@ -0,0 +1,158 @@
+use std::collections::HashMap;
+use std::path::Path;
+
+use async_process::Command;
+
+use kanit_common::error::{Context, Result};
+
+pub async fn is_fs_available(fs: &str) -> Result<bool> {
+ let filesystems = async_fs::read_to_string("/proc/filesystems")
+ .await
+ .context("failed to read filesystems")?;
+
+ // prepend tab as the format is `nodev <fs>` or ` <fs>`
+ // TODO; maybe something a bit more elegant
+ Ok(filesystems.contains(&format!("\t{fs}")))
+}
+
+pub async fn is_fs_mounted<P: AsRef<Path>>(path: P) -> Result<bool> {
+ let mounted = async_fs::read_to_string("/proc/mounts")
+ .await
+ .context("failed to read mounts")?;
+
+ let path = path.as_ref().to_string_lossy();
+
+ Ok(parse_mounts(&mounted)?.iter().any(|m| m.fs_file == path))
+}
+
+pub async fn try_mount_from_fstab<P: AsRef<Path>>(path: P) -> Result<bool> {
+ try_mount_from_fstab_action(path, MountAction::Mount).await
+}
+
+pub async fn try_mount_from_fstab_action<P: AsRef<Path>>(
+ path: P,
+ action: MountAction,
+) -> Result<bool> {
+ let fstab = async_fs::read_to_string("/etc/fstab")
+ .await
+ .context("failed to read fstab")?;
+
+ let path = path.as_ref().to_string_lossy();
+
+ if let Some(entry) = parse_mounts(&fstab)?.iter().find(|m| m.fs_file == path) {
+ Ok(entry.mount(action).await?)
+ } else {
+ Ok(false)
+ }
+}
+
+pub struct MountEntry<'a> {
+ pub fs_spec: &'a str,
+ pub fs_file: &'a str,
+ pub fs_vfstype: &'a str,
+ pub fs_mntopts: HashMap<&'a str, Option<&'a str>>,
+ // used by dump(8) and fsck(8)
+ pub _fs_freq: u8,
+ pub _fs_passno: u8,
+}
+
+pub enum MountAction {
+ Mount,
+ Remount,
+}
+
+impl<'a> MountEntry<'a> {
+ pub fn parse_single_mount(line: &'a str) -> Result<Self> {
+ let mut parts = line.split_whitespace();
+
+ let fs_spec = parts.next().context("expected `fs_spec`")?;
+
+ let fs_file = parts.next().context("expected `fs_file`")?;
+
+ let fs_vfstype = parts.next().context("expected `fs_vfstype`")?;
+
+ let fs_mntopts = parts.next().context("expected `fs_mntopts`")?;
+
+ let fs_mntopts: HashMap<&str, Option<&str>> = fs_mntopts
+ .split(',')
+ .map(|s| {
+ let mut split = s.splitn(2, '=');
+ // unwrap: split will always have at least 1
+ let opt = split.next().unwrap();
+ let val = split.next();
+
+ (opt, val)
+ })
+ .collect();
+
+ let fs_freq = parts
+ .next()
+ .context("expected `fs_freq`")?
+ .parse::<u8>()
+ .context("failed to parse `fs_freq`")?;
+
+ let fs_passno = parts
+ .next()
+ .context("expected `fs_passno`")?
+ .parse::<u8>()
+ .context("failed to parse `fs_passno`")?;
+
+ Ok(Self {
+ fs_spec,
+ fs_file,
+ fs_vfstype,
+ fs_mntopts,
+ _fs_freq: fs_freq,
+ _fs_passno: fs_passno,
+ })
+ }
+
+ pub async fn mount(&self, action: MountAction) -> Result<bool> {
+ let mut opts = self
+ .fs_mntopts
+ .iter()
+ .map(|(k, v)| {
+ if let Some(v) = v {
+ format!("{k}={v}")
+ } else {
+ k.to_string()
+ }
+ })
+ .collect::<Vec<_>>()
+ .join(",");
+
+ if let MountAction::Remount = action {
+ if opts.is_empty() {
+ opts.push_str("remount");
+ } else {
+ opts.push_str(",remount");
+ }
+ }
+
+ Ok(Command::new("mount")
+ .arg("-o")
+ .arg(opts)
+ .arg("-t")
+ .arg(self.fs_vfstype)
+ .arg(self.fs_spec)
+ .arg(self.fs_file)
+ .spawn()
+ .context("failed to start mount")?
+ .status()
+ .await
+ .context("failed to wait on mount")?
+ .success())
+ }
+}
+
+pub fn parse_mounts(lines: &str) -> Result<Vec<MountEntry>> {
+ lines
+ .lines()
+ .filter_map(|line| {
+ if line.starts_with('#') || line.is_empty() {
+ return None;
+ }
+ Some(MountEntry::parse_single_mount(line))
+ })
+ .collect()
+}
diff --git a/crates/units/src/oneshot/cgroup.rs b/crates/units/src/oneshot/cgroup.rs
@@ -0,0 +1,77 @@
+use std::path::Path;
+
+use async_trait::async_trait;
+use blocking::unblock;
+use log::info;
+use nix::mount::{MsFlags, mount};
+use nix::sys::stat::Mode;
+use nix::unistd::mkdir;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::mounts::{is_fs_available, try_mount_from_fstab};
+use crate::oneshot::SysFs;
+use crate::unit_name;
+
+pub struct Cgroup;
+
+impl Cgroup {
+ // TODO; cgroup1
+ async fn mount_cgroup2() -> Result<()> {
+ if !is_fs_available("cgroup2").await? {
+ Err(StaticError("failed to mount cgroup")).kind(ErrorKind::Unrecoverable)?;
+ };
+
+ info!("mounting cgroup2");
+
+ let path = Path::new("/sys/fs/cgroup");
+
+ if try_mount_from_fstab(path).await? {
+ return Ok(());
+ }
+
+ unblock(move || {
+ mount(
+ Some("none"),
+ Path::new("/sys/fs/cgroup"),
+ Some("cgroup2"),
+ MsFlags::MS_NODEV | MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC,
+ Some("nsdelegate,memory_recursiveprot"),
+ )
+ })
+ .await
+ .context("failed to mount cgroup2")?;
+
+ Ok(())
+ }
+
+ async fn init_cgroups() -> Result<()> {
+ let path = Path::new("/sys/fs/cgroup");
+
+ unblock(move || {
+ mkdir(
+ &path.join("system.slice"),
+ Mode::S_IRWXU | Mode::S_IRGRP | Mode::S_IRUSR,
+ )
+ })
+ .await
+ .context("failed to create system.slice")?;
+
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl Unit for Cgroup {
+ unit_name!("cgroup");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().need(SysFs.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ Self::mount_cgroup2().await?;
+ Self::init_cgroups().await
+ }
+}
diff --git a/crates/units/src/oneshot/clock.rs b/crates/units/src/oneshot/clock.rs
@@ -0,0 +1,142 @@
+use std::path::Path;
+
+use async_process::{Command, Stdio};
+use async_trait::async_trait;
+use futures_lite::StreamExt;
+use log::{info, warn};
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::Modules;
+use crate::unit_name;
+
+// chronos <3 - Sparkles
+pub struct Clock;
+
+fn check_rtc(name: &Path) -> bool {
+ let f_str = name.to_string_lossy();
+
+ if f_str == "/dev/rtc"
+ || f_str.starts_with("/dev/rtc")
+ && f_str
+ .chars()
+ .nth(8)
+ .map(|c| c.is_ascii_digit())
+ .unwrap_or(false)
+ {
+ return true;
+ }
+
+ false
+}
+
+impl Clock {
+ async fn rtc_exists() -> Result<bool> {
+ Ok(async_fs::read_dir("/dev")
+ .await
+ .context("failed to open /dev")?
+ .filter_map(|res| res.map(|e| e.path()).ok())
+ .find(|p| check_rtc(p))
+ .await
+ .is_some())
+ }
+}
+
+const RTC_MODS: [&str; 3] = ["rtc-cmos", "rtc", "genrtc"];
+
+#[async_trait]
+impl Unit for Clock {
+ unit_name!("clock");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().want(Modules.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("setting time with hardware clock");
+
+ if !Self::rtc_exists().await? {
+ let mut loaded = false;
+
+ for module in RTC_MODS {
+ let succ = Command::new("modprobe")
+ .args(["-q", module])
+ .spawn()
+ .context("failed to spawn modprobe")?
+ .status()
+ .await
+ .context("failed to wait")?
+ .success();
+
+ if succ && Self::rtc_exists().await? {
+ warn!("module {module} should be built in or configured to load");
+ loaded = true;
+ break;
+ }
+ }
+
+ if !loaded {
+ Err(StaticError("failed to set hardware clock")).kind(ErrorKind::Recoverable)?;
+ }
+ }
+
+ // todo; UTC
+
+ let succ = Command::new("hwclock")
+ .args(["--systz", "--localtime"])
+ .stderr(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .context("failed to spawn hwclock")?
+ .output()
+ .await
+ .context("failed to wait")?
+ .stderr
+ .is_empty();
+
+ if !succ {
+ warn!("failed to set system timezone");
+ }
+
+ let succ = Command::new("hwclock")
+ .args(["--hctosys", "--localtime"])
+ .stderr(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .context("failed to spawn hwclock")?
+ .output()
+ .await
+ .context("failed to wait")?
+ .stderr
+ .is_empty();
+
+ if !succ {
+ warn!("failed to set system time");
+ }
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ info!("setting hardware clock with system time");
+
+ let succ = Command::new("hwclock")
+ .args(["--systohc", "--localtime"])
+ .stderr(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()
+ .context("failed to spawn hwclock")?
+ .output()
+ .await
+ .context("failed to wait")?
+ .stderr
+ .is_empty();
+
+ if !succ {
+ warn!("failed to set hardware clock");
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/devfs.rs b/crates/units/src/oneshot/devfs.rs
@@ -0,0 +1,240 @@
+use std::os::unix::fs::FileTypeExt;
+use std::path::Path;
+
+use async_fs::unix::symlink;
+use async_trait::async_trait;
+use blocking::unblock;
+use libc::dev_t;
+use log::info;
+use nix::mount::{MsFlags, mount};
+use nix::sys::stat::{Mode, SFlag, makedev, mknod};
+use nix::unistd::mkdir;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_executor::join_all;
+use kanit_unit::{Dependencies, Unit};
+
+use crate::mounts::{
+ MountAction, is_fs_available, is_fs_mounted, try_mount_from_fstab, try_mount_from_fstab_action,
+};
+use crate::oneshot::MDev;
+use crate::unit_name;
+
+pub struct DevFs;
+
+async fn character_device<P: AsRef<Path>>(path: P) -> bool {
+ async_fs::metadata(path)
+ .await
+ .ok()
+ .map(|m| m.file_type().is_char_device())
+ .unwrap_or(false)
+}
+
+async fn exists<P: AsRef<Path>>(path: P) -> bool {
+ async_fs::metadata(path).await.is_ok()
+}
+
+async fn mount_opt(
+ path: &'static str,
+ name: &'static str,
+ mode: Mode,
+ flags: MsFlags,
+ opts: &'static str,
+) -> Result<()> {
+ if is_fs_available(name).await? && !is_fs_mounted(&path).await? {
+ if !exists(path).await {
+ unblock(move || mkdir(path, mode))
+ .await
+ .context("failed to make directory")?;
+ }
+
+ info!("mounting {path}");
+
+ if try_mount_from_fstab(&path).await? {
+ return Ok(());
+ }
+
+ unblock(move || {
+ mount(
+ Some("none"),
+ path,
+ Some(name),
+ flags | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID,
+ Some(opts),
+ )
+ })
+ .await
+ .with_context(move || format!("failed to mount {name}"))?;
+ }
+
+ Ok(())
+}
+
+async fn dev_device(path: &'static str, kind: SFlag, perm: Mode, dev: dev_t) -> Result<()> {
+ if character_device(path).await {
+ return Ok(());
+ }
+
+ unblock(move || mknod(path, kind, perm, dev))
+ .await
+ .with_context(move || format!("failed to create {path}"))
+}
+
+async fn sym(src: &'static str, dst: &'static str) -> Result<()> {
+ if !exists(dst).await {
+ symlink(src, dst)
+ .await
+ .with_context(move || format!("failed to link {dst}"))?;
+ }
+
+ Ok(())
+}
+
+impl DevFs {
+ async fn mount_dev() -> Result<()> {
+ let path = Path::new("/dev");
+
+ let mut opts = MsFlags::MS_NOSUID;
+ let mut action = MountAction::Mount;
+
+ if is_fs_mounted(path).await? {
+ info!("remounting devfs");
+ opts |= MsFlags::MS_REMOUNT;
+ action = MountAction::Remount;
+ } else {
+ info!("mounting devfs");
+ }
+
+ if try_mount_from_fstab_action(path, action).await? {
+ return Ok(());
+ }
+
+ let fs = if is_fs_available("devtmpfs").await? {
+ "devtmpfs"
+ } else if is_fs_available("tmpfs").await? {
+ "tmpfs"
+ } else {
+ Err(StaticError(
+ "devtmpfs, tmpfs, nor fstab entry available, /dev will not be mounted",
+ ))
+ .kind(ErrorKind::Recoverable)?
+ };
+
+ unblock(move || mount(Some("none"), path, Some(fs), opts, Some("")))
+ .await
+ .context("failed to mount devfs")?;
+
+ Ok(())
+ }
+
+ async fn populate_dev() -> Result<()> {
+ // create dev devices if they don't exist
+
+ join_all([
+ dev_device(
+ "/dev/console",
+ SFlag::S_IFCHR,
+ Mode::S_IRUSR | Mode::S_IWUSR,
+ makedev(5, 1),
+ ),
+ dev_device(
+ "/dev/tty1",
+ SFlag::S_IFCHR,
+ Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IWGRP,
+ makedev(4, 1),
+ ),
+ dev_device(
+ "/dev/tty",
+ SFlag::S_IFCHR,
+ Mode::S_IRUSR
+ | Mode::S_IWUSR
+ | Mode::S_IRGRP
+ | Mode::S_IWGRP
+ | Mode::S_IROTH
+ | Mode::S_IWOTH,
+ makedev(4, 1),
+ ),
+ dev_device(
+ "/dev/null",
+ SFlag::S_IFCHR,
+ Mode::S_IRUSR
+ | Mode::S_IWUSR
+ | Mode::S_IRGRP
+ | Mode::S_IWGRP
+ | Mode::S_IROTH
+ | Mode::S_IWOTH,
+ makedev(1, 3),
+ ),
+ dev_device(
+ "/dev/kmsg",
+ SFlag::S_IFCHR,
+ Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IWGRP,
+ makedev(1, 11),
+ ),
+ ])
+ .await
+ .into_iter()
+ .collect::<Result<Vec<_>>>()?;
+
+ join_all([
+ sym("/proc/self/fd", "/dev/fd"),
+ sym("/proc/self/fd/0", "/dev/stdin"),
+ sym("/proc/self/fd/1", "/dev/stdout"),
+ sym("/proc/self/fd/2", "/dev/stderr"),
+ ])
+ .await
+ .into_iter()
+ .collect::<Result<Vec<_>>>()?;
+
+ join_all([
+ mount_opt(
+ "/dev/mqueue",
+ "mqueue",
+ Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO | Mode::S_ISVTX,
+ MsFlags::MS_NODEV,
+ "",
+ ),
+ mount_opt(
+ "/dev/pts",
+ "devpts",
+ Mode::S_IRWXU | Mode::S_IRGRP | Mode::S_IXGRP | Mode::S_IROTH | Mode::S_IXOTH,
+ MsFlags::empty(),
+ "gid=5,mode=0620",
+ ),
+ mount_opt(
+ "/dev/shm",
+ "tmpfs",
+ Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO | Mode::S_ISVTX,
+ MsFlags::MS_NODEV,
+ "",
+ ),
+ ])
+ .await
+ .into_iter()
+ .collect::<Result<Vec<_>>>()?;
+
+ if Path::new("/proc/kcore").exists() {
+ symlink("/proc/kcore", "/dev/core")
+ .await
+ .context("failed to link kcore")?;
+ }
+
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl Unit for DevFs {
+ unit_name!("devfs");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().before(MDev.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ Self::mount_dev().await?;
+ Self::populate_dev().await?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/hostname.rs b/crates/units/src/oneshot/hostname.rs
@@ -0,0 +1,32 @@
+use async_trait::async_trait;
+use blocking::unblock;
+use nix::unistd::sethostname;
+
+use kanit_common::error::{Context, ErrorKind, Result};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::Clock;
+use crate::unit_name;
+
+pub struct Hostname;
+
+#[async_trait]
+impl Unit for Hostname {
+ unit_name!("hostname");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().after(Clock.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ let hostname = async_fs::read_to_string("/etc/hostname")
+ .await
+ .unwrap_or_else(|_| "homosexual".to_string());
+
+ unblock(move || sethostname(hostname.trim_start().trim_end()))
+ .await
+ .context_kind("failed to set hostname", ErrorKind::Recoverable)?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/hwdrivers.rs b/crates/units/src/oneshot/hwdrivers.rs
@@ -0,0 +1,70 @@
+use std::collections::HashSet;
+
+use async_process::{Command, Stdio};
+use async_trait::async_trait;
+use blocking::unblock;
+use futures_lite::StreamExt;
+use futures_lite::stream::iter;
+use log::info;
+use walkdir::WalkDir;
+
+use kanit_common::error::{Context, Result};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::{DevFs, SysFs};
+use crate::unit_name;
+
+pub struct HwDrivers;
+
+impl HwDrivers {
+ async fn load_modules() -> Result<()> {
+ // todo; better walker
+ let modules = iter(
+ unblock(move || {
+ WalkDir::new("/sys")
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.file_type().is_file() && e.file_name() == "modalias")
+ })
+ .await,
+ )
+ .then(|e| async { async_fs::read_to_string(e.into_path()).await })
+ .filter_map(|e| e.ok())
+ .collect::<HashSet<String>>()
+ .await;
+
+ // we don't care about status
+ Command::new("modprobe")
+ .stderr(Stdio::null())
+ .args(["-b", "-a"])
+ .args(modules)
+ .spawn()
+ .context("failed to spawn modprobe")?
+ .status()
+ .await
+ .context("failed to wait")?;
+
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl Unit for HwDrivers {
+ unit_name!("hwdrivers");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ .need(DevFs.name())
+ .need(SysFs.name())
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("loading modules for hardware");
+
+ Self::load_modules().await?;
+ Self::load_modules().await?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/localmount.rs b/crates/units/src/oneshot/localmount.rs
@@ -0,0 +1,127 @@
+use async_process::{Command, Stdio};
+use async_trait::async_trait;
+use log::info;
+use nix::unistd::sync;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::mounts::{is_fs_mounted, parse_mounts};
+use crate::oneshot::{Clock, Modules, RootFs};
+use crate::unit_name;
+
+pub struct LocalMount;
+
+async fn kill_fs_users(fs: &str) -> Result<()> {
+ Command::new("fuser")
+ .args(["-KILL", "-k", "-m", fs])
+ .spawn()
+ .context("failed to spawn fuser")?
+ .status()
+ .await
+ .context("failed to wait fuser")?;
+
+ Ok(())
+}
+
+#[async_trait]
+impl Unit for LocalMount {
+ unit_name!("localmount");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ .need(RootFs.name())
+ .after(Clock.name())
+ .after(Modules.name())
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("mounting local filesystems");
+
+ let succ = Command::new("mount")
+ .stdout(Stdio::null())
+ .args(["-a", "-t", "noproc"])
+ .spawn()
+ .context("failed to spawn mount")?
+ .status()
+ .await
+ .context("failed to wait on mount")?
+ .success();
+
+ if !succ {
+ Err(StaticError("failed to mount local filesystems")).kind(ErrorKind::Recoverable)?;
+ }
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ sync();
+
+ let mounted = async_fs::read_to_string("/proc/mounts")
+ .await
+ .context("failed to read mounts")?;
+ // loopback
+ info!("unmounting loopback");
+
+ for mount in parse_mounts(&mounted)? {
+ if !is_fs_mounted(mount.fs_file).await.unwrap_or(false) {
+ continue;
+ }
+
+ if !mount.fs_file.starts_with("/dev/loop") {
+ continue;
+ }
+
+ // should already be dead since init should've killed
+ kill_fs_users(mount.fs_file).await?;
+
+ Command::new("umount")
+ .args(["-d", mount.fs_file])
+ .spawn()
+ .context("failed to spawn umount")?
+ .status()
+ .await
+ .context("failed to wait umount")?;
+ }
+
+ // now everything but network
+ info!("unmounting filesystems");
+ for mount in parse_mounts(&mounted)? {
+ let n = mount.fs_file;
+
+ if !is_fs_mounted(n).await.unwrap_or(false) {
+ continue;
+ }
+
+ if n == "/"
+ || n == "/dev"
+ || n == "/sys"
+ || n == "/proc"
+ || n == "/run"
+ || n.starts_with("/dev/")
+ || n.starts_with("/sys/")
+ || n.starts_with("/proc/")
+ {
+ continue;
+ }
+
+ if mount.fs_mntopts.contains_key("_netdev") {
+ continue;
+ }
+
+ kill_fs_users(mount.fs_file).await?;
+
+ Command::new("umount")
+ .arg(mount.fs_file)
+ .spawn()
+ .context("failed to spawn umount")?
+ .status()
+ .await
+ .context("failed to wait umount")?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/mdev.rs b/crates/units/src/oneshot/mdev.rs
@@ -0,0 +1,59 @@
+use async_process::Command;
+use async_trait::async_trait;
+use log::info;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::{DevFs, SysFs};
+use crate::unit_name;
+
+// TODO; write one for udev as well
+pub struct MDev;
+
+#[async_trait]
+impl Unit for MDev {
+ unit_name!("mdev");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ .need(SysFs.name())
+ .need(DevFs.name())
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("initializing mdev");
+
+ async_fs::write("/proc/sys/kernel/hotplug", "/sbin/mdev")
+ .await
+ .context("failed to initialize mdev for hotplug")?;
+
+ info!("loading hardware for mdev");
+
+ let succ = Command::new("mdev")
+ .arg("-s")
+ .spawn()
+ .context("failed to spawn mdev")?
+ .status()
+ .await
+ .context("failed to wait")?
+ .success();
+
+ if !succ {
+ Err(StaticError("failed to spawn mdev")).kind(ErrorKind::Recoverable)?;
+ }
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ info!("stopping mdev");
+
+ async_fs::write("/proc/sys/kernel/hotplug", "/sbin/mdev")
+ .await
+ .context_kind("failed to stop mdev", ErrorKind::Recoverable)?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/mod.rs b/crates/units/src/oneshot/mod.rs
@@ -0,0 +1,46 @@
+pub use cgroup::Cgroup;
+pub use clock::Clock;
+pub use devfs::DevFs;
+pub use hostname::Hostname;
+pub use hwdrivers::HwDrivers;
+pub use localmount::LocalMount;
+pub use mdev::MDev;
+pub use modules::Modules;
+pub use procfs::ProcFs;
+pub use rootfs::RootFs;
+pub use run::Run;
+pub use seed::Seed;
+pub use swap::Swap;
+pub use sysfs::SysFs;
+
+mod cgroup;
+mod clock;
+mod devfs;
+mod hostname;
+mod hwdrivers;
+mod localmount;
+mod mdev;
+mod modules;
+mod procfs;
+mod rootfs;
+mod run;
+mod seed;
+mod swap;
+mod sysctl;
+mod sysfs;
+
+// deduplication of unit names
+// as unit names are used as keys and things, having them deduplicated would be good
+// it also allows comparisons to be O(1) by comparing addresses
+#[macro_export]
+macro_rules! unit_name {
+ ($name:literal) => {
+ fn name(&self) -> kanit_unit::UnitName {
+ static NAME: std::sync::OnceLock<kanit_unit::UnitName> = std::sync::OnceLock::new();
+
+ return NAME
+ .get_or_init(|| kanit_unit::UnitName::from($name))
+ .clone();
+ }
+ };
+}
diff --git a/crates/units/src/oneshot/modules.rs b/crates/units/src/oneshot/modules.rs
@@ -0,0 +1,77 @@
+use std::os::unix::ffi::OsStrExt;
+
+use async_process::Command;
+use async_trait::async_trait;
+use blocking::unblock;
+use futures_lite::StreamExt;
+use futures_lite::stream::iter;
+use log::{info, warn};
+use walkdir::WalkDir;
+
+use kanit_common::error::{Context, Result};
+use kanit_unit::Unit;
+
+use crate::unit_name;
+
+pub struct Modules;
+
+const LOADED_FOLDERS: [&str; 3] = [
+ "/etc/modules-load.d/",
+ "/run/modules-load./",
+ "/usr/lib/modules-load.d/",
+];
+
+#[async_trait]
+impl Unit for Modules {
+ unit_name!("modules");
+
+ async fn start(&mut self) -> Result<()> {
+ if async_fs::metadata("/proc/modules").await.is_err() {
+ return Ok(());
+ }
+
+ for dir in LOADED_FOLDERS.iter() {
+ let modules = iter(
+ unblock(move || {
+ WalkDir::new(dir)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| {
+ e.file_type().is_file() && e.file_name().as_bytes().ends_with(b".conf")
+ })
+ })
+ .await,
+ )
+ .then(|e| async { async_fs::read_to_string(e.into_path()).await })
+ .filter_map(|e| e.ok())
+ .collect::<Vec<String>>()
+ .await
+ .into_iter()
+ .flat_map(|e| {
+ e.lines()
+ .filter(|l| !l.starts_with('#') && !l.starts_with(';') && !l.is_empty())
+ .map(|s| s.to_string())
+ .collect::<Vec<String>>()
+ });
+
+ for module in modules {
+ info!("loading module {module}");
+
+ let succ = Command::new("modprobe")
+ .args(["-b", "-a", "-v", &module])
+ .spawn()
+ .context("failed to spawn modprobe")?
+ .status()
+ .await
+ .context("failed to wait")?
+ .success();
+
+ if !succ {
+ warn!("failed to load module {module}");
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/procfs.rs b/crates/units/src/oneshot/procfs.rs
@@ -0,0 +1,49 @@
+use std::path::Path;
+
+use async_trait::async_trait;
+use blocking::unblock;
+use log::info;
+use nix::mount::{MsFlags, mount};
+
+use kanit_common::error::{Context, Result};
+use kanit_unit::Unit;
+
+use crate::mounts::try_mount_from_fstab;
+use crate::unit_name;
+
+pub struct ProcFs;
+
+#[async_trait]
+impl Unit for ProcFs {
+ unit_name!("procfs");
+
+ async fn start(&mut self) -> Result<()> {
+ // check if proc is already mounted
+ if Path::new("/proc/mounts").exists() {
+ info!("procfs already mounted");
+ return Ok(());
+ }
+
+ info!("mounting /proc");
+
+ let path = Path::new("/proc");
+
+ if try_mount_from_fstab(path).await? {
+ return Ok(());
+ }
+
+ unblock(move || {
+ mount(
+ Some("none"),
+ path,
+ Some("proc"),
+ MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID,
+ Some(""),
+ )
+ })
+ .await
+ .context("failed to mount procfs")?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/rootfs.rs b/crates/units/src/oneshot/rootfs.rs
@@ -0,0 +1,63 @@
+use async_trait::async_trait;
+use blocking::unblock;
+use log::{info, warn};
+use nix::mount::{MsFlags, mount};
+use nix::unistd::{AccessFlags, access};
+
+use kanit_common::error::{Context, ErrorKind, Result};
+use kanit_executor::join_all;
+use kanit_unit::{Dependencies, Unit};
+
+use crate::mounts::{MountAction, MountEntry, is_fs_mounted, parse_mounts};
+use crate::oneshot::Clock;
+use crate::unit_name;
+
+pub struct RootFs;
+
+async fn remount_entry(entry: MountEntry<'_>) -> Result<()> {
+ if is_fs_mounted(entry.fs_file).await? && !entry.mount(MountAction::Remount).await? {
+ warn!("failed to remount {}", entry.fs_file);
+ }
+
+ Ok(())
+}
+
+#[async_trait]
+impl Unit for RootFs {
+ unit_name!("rootfs");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().after(Clock.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ if access("/", AccessFlags::W_OK).is_ok() {
+ return Ok(()); // rootfs already writable
+ }
+
+ info!("remounting rootfs as rw");
+
+ unblock(move || {
+ mount(
+ Some(""), // ignored in remount
+ "/",
+ Some(""),
+ MsFlags::MS_REMOUNT,
+ Some("rw"),
+ )
+ })
+ .await
+ .context("failed to remount rootfs")?;
+
+ info!("remounting filesystems");
+
+ let fstab = async_fs::read_to_string("/etc/fstab")
+ .await
+ .context_kind("failed to read fstab", ErrorKind::Recoverable)?;
+
+ // TODO; leak probably bad
+ join_all(parse_mounts(fstab.leak())?.into_iter().map(remount_entry)).await;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/run.rs b/crates/units/src/oneshot/run.rs
@@ -0,0 +1,77 @@
+use std::path::Path;
+
+use async_trait::async_trait;
+use blocking::unblock;
+use log::info;
+use nix::mount::{MsFlags, mount};
+use nix::sys::stat::Mode;
+use nix::unistd::{Group, Uid, chown, mkdir};
+
+use kanit_common::constants;
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::mounts::try_mount_from_fstab;
+use crate::oneshot::ProcFs;
+use crate::unit_name;
+
+pub struct Run;
+
+#[async_trait]
+impl Unit for Run {
+ unit_name!("run");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().need(ProcFs.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ let path = Path::new("/run");
+
+ if !path.exists() {
+ Err(StaticError("/run doesn't exist")).kind(ErrorKind::Unrecoverable)?;
+ }
+
+ info!("mounting /run");
+
+ if try_mount_from_fstab(path).await? {
+ return Ok(());
+ }
+
+ unblock(move || {
+ mount(
+ Some("none"),
+ path,
+ Some("tmpfs"),
+ MsFlags::MS_NODEV | MsFlags::MS_STRICTATIME | MsFlags::MS_NOSUID,
+ Some("mode=0755,nr_inodes=500k,size=10%"),
+ )
+ })
+ .await
+ .context("failed to mount run")?;
+
+ info!("creating /run/lock");
+
+ let lock = path.join("lock");
+
+ unblock(move || -> Result<()> {
+ mkdir(&lock, Mode::S_IROTH | Mode::S_IXOTH | Mode::S_IWUSR)?;
+
+ let gid = Group::from_name("uucp")
+ .context("failed to get group uucp")?
+ .map(|g| g.gid);
+
+ chown(&lock, Some(Uid::from_raw(0)), gid)
+ .context("failed to set permissions on /run/lock")?;
+
+ Ok(())
+ })
+ .await?;
+
+ info!("creating {}", constants::KAN_PIDS);
+
+ unblock(move || mkdir(constants::KAN_PIDS, Mode::S_IRUSR | Mode::S_IWUSR))
+ .await
+ .context("failed to create pid directory")
+ }
+}
diff --git a/crates/units/src/oneshot/seed.rs b/crates/units/src/oneshot/seed.rs
@@ -0,0 +1,190 @@
+use std::cmp::Ordering;
+use std::mem::size_of;
+use std::os::unix::fs::PermissionsExt;
+
+use async_fs::File;
+use async_trait::async_trait;
+use blocking::unblock;
+use futures_lite::{AsyncReadExt, AsyncWriteExt};
+use log::info;
+use nix::errno::Errno;
+use nix::fcntl::{OFlag, open};
+use nix::request_code_write;
+use nix::sys::stat::Mode;
+use nix::sys::time::{TimeSpec, TimeValLike};
+use nix::time::{ClockId, clock_gettime};
+use nix::unistd::close;
+
+use kanit_common::constants;
+use kanit_common::error::{Context, ErrorKind, Result};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::{Clock, LocalMount};
+use crate::unit_name;
+
+pub struct Seed;
+
+const RND_IOC_MAGIC: u8 = b'R';
+const RND_IOC_TYPE_MODE: u8 = 0x03;
+const MAX_SEED_LEN: usize = 512;
+
+#[repr(C)]
+struct RandPoolInfo {
+ entropy_count: libc::c_int,
+ buf_size: libc::c_int,
+ buf: [u8; MAX_SEED_LEN], // defined as u32 but it doesn't *really* matter
+}
+
+impl Seed {
+ fn untrusted_seed() -> u64 {
+ let seconds = clock_gettime(ClockId::CLOCK_REALTIME) // TODO; unblock?
+ .unwrap_or_else(|_| {
+ clock_gettime(ClockId::CLOCK_BOOTTIME).unwrap_or(TimeSpec::new(0, 0))
+ })
+ .num_seconds();
+
+ u64::from_be_bytes(seconds.to_be_bytes())
+ }
+
+ async fn trusted_seed() -> Option<u64> {
+ if let Ok(mut data) = async_fs::read(constants::KAN_SEED).await {
+ data.resize(8, 0);
+
+ // since the vector was resized, this shouldn't fail
+ Some(u64::from_be_bytes(data.as_slice().try_into().unwrap()))
+ } else {
+ None
+ }
+ }
+
+ async fn write_bytes(bytes: &[u8], trusted: bool) -> Result<()> {
+ let buf: [u8; MAX_SEED_LEN] = match bytes.len().cmp(&MAX_SEED_LEN) {
+ Ordering::Greater => (&bytes[0..MAX_SEED_LEN]).try_into().unwrap(),
+ Ordering::Less => {
+ let mut buf = [0u8; MAX_SEED_LEN];
+
+ let view = &mut buf[0..bytes.len()];
+
+ view.copy_from_slice(bytes);
+
+ buf
+ }
+ Ordering::Equal => bytes.try_into().unwrap(),
+ };
+
+ let entropy_count = if trusted { bytes.len() * 8 } else { 0 };
+
+ let info = RandPoolInfo {
+ entropy_count: entropy_count as libc::c_int,
+ buf_size: bytes.len() as libc::c_int,
+ buf, // unwrap: we ensure the size of `bytes`
+ };
+
+ unblock(move || {
+ let rand_fd = open("/dev/urandom", OFlag::O_RDONLY, Mode::empty())
+ .context_kind("failed to open urandom", ErrorKind::Recoverable)?;
+
+ unsafe {
+ Errno::result(libc::ioctl(
+ rand_fd,
+ request_code_write!(
+ RND_IOC_MAGIC,
+ RND_IOC_TYPE_MODE,
+ size_of::<libc::c_int>() * 2
+ ),
+ &info as *const RandPoolInfo,
+ ))
+ }
+ .context_kind("failed to add entropy", ErrorKind::Recoverable)?;
+
+ close(rand_fd).context_kind("failed to close urandom", ErrorKind::Recoverable)?;
+
+ Ok(())
+ })
+ .await
+ }
+}
+
+#[async_trait]
+impl Unit for Seed {
+ unit_name!("seed");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ .need(LocalMount.name())
+ .after(Clock.name())
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("seeding random number generator");
+
+ let t_seed = Self::trusted_seed().await;
+
+ let seed = t_seed.unwrap_or_else(Self::untrusted_seed);
+
+ let mut rand = fastrand::Rng::with_seed(seed);
+
+ let bytes = async_fs::read_to_string("/proc/sys/kernel/random/poolsize")
+ .await
+ .unwrap_or_else(|_| "2048".to_string())
+ .parse::<usize>()
+ .unwrap_or(2048)
+ / 8;
+
+ // preload some bytes
+ let mut buf = Vec::with_capacity(bytes);
+
+ rand.fill(buf.as_mut_slice());
+
+ Self::write_bytes(buf.as_slice(), t_seed.is_some()).await?;
+
+ // load previous seed
+ if let Ok(mut previous_seed) = async_fs::read(constants::KAN_SEED).await {
+ let len = previous_seed.len();
+
+ previous_seed.resize(bytes, 0);
+
+ if bytes > len {
+ rand.fill(&mut previous_seed[len..]);
+ }
+
+ Self::write_bytes(previous_seed.as_slice(), true).await?;
+ }
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ info!("saving random seed");
+
+ let mut f = File::open("/dev/urandom")
+ .await
+ .context_kind("failed to open urandom", ErrorKind::Recoverable)?;
+
+ let mut buf = [0; 16];
+
+ f.read_exact(&mut buf)
+ .await
+ .context_kind("failed to save random seed", ErrorKind::Recoverable)?;
+
+ let mut file = File::create(constants::KAN_SEED)
+ .await
+ .context_kind("failed to open seed file", ErrorKind::Recoverable)?;
+
+ let metadata = file
+ .metadata()
+ .await
+ .context_kind("failed to get metadata", ErrorKind::Recoverable)?;
+
+ metadata
+ .permissions()
+ .set_mode((Mode::S_IWUSR | Mode::S_IRUSR).bits());
+
+ file.write(&buf)
+ .await
+ .context_kind("failed to write seed", ErrorKind::Recoverable)?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/swap.rs b/crates/units/src/oneshot/swap.rs
@@ -0,0 +1,64 @@
+use async_process::{Command, Stdio};
+use async_trait::async_trait;
+use log::info;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::{Clock, LocalMount, RootFs};
+use crate::unit_name;
+
+pub struct Swap;
+
+#[async_trait]
+impl Unit for Swap {
+ unit_name!("swap");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ .need(RootFs.name())
+ .after(Clock.name())
+ .before(LocalMount.name())
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("mounting swap");
+
+ let succ = Command::new("swapon")
+ .stdout(Stdio::null())
+ .arg("-a")
+ .spawn()
+ .context("failed to spawn swapon")?
+ .status()
+ .await
+ .context("failed to wait on swapon")?
+ .success();
+
+ if !succ {
+ Err(StaticError("failed to enable swap")).kind(ErrorKind::Recoverable)?;
+ }
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ info!("unmounting swap");
+
+ let succ = Command::new("swapoff")
+ .stdout(Stdio::null())
+ .arg("-a")
+ .spawn()
+ .context("failed to spawn swapon")?
+ .status()
+ .await
+ .context("failed to wait on swapon")?
+ .success();
+
+ if !succ {
+ Err(StaticError("failed to disable swap")).kind(ErrorKind::Recoverable)?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/sysctl.rs b/crates/units/src/oneshot/sysctl.rs
@@ -0,0 +1,40 @@
+use async_process::{Command, Stdio};
+use async_trait::async_trait;
+use log::info;
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::Clock;
+use crate::unit_name;
+
+pub struct Sysctl;
+
+#[async_trait]
+impl Unit for Sysctl {
+ unit_name!("sysctl");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().after(Clock.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("loading sysctl");
+
+ let succ = Command::new("sysctl")
+ .stdout(Stdio::null())
+ .args(["-q", "--system"])
+ .spawn()
+ .context("failed to spawn sysctl")?
+ .status()
+ .await
+ .context("failed to wait on sysctl")?
+ .success();
+
+ if !succ {
+ Err(StaticError("failed load sysctl")).kind(ErrorKind::Recoverable)?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/oneshot/sysfs.rs b/crates/units/src/oneshot/sysfs.rs
@@ -0,0 +1,125 @@
+use std::path::Path;
+
+use async_trait::async_trait;
+use blocking::unblock;
+use log::{info, warn};
+use nix::mount::{MsFlags, mount};
+
+use kanit_common::error::{Context, ErrorKind, Result, StaticError};
+use kanit_executor::join_all;
+use kanit_unit::{Dependencies, Unit};
+
+use crate::mounts::{is_fs_available, is_fs_mounted, try_mount_from_fstab};
+use crate::oneshot::ProcFs;
+use crate::unit_name;
+
+pub struct SysFs;
+
+async fn mount_misc_fs<P: AsRef<Path>>(path: P, name: &'static str) -> Result<()> {
+ let path = path.as_ref().to_owned();
+
+ if path.exists() && is_fs_available(name).await? && !is_fs_mounted(&path).await? {
+ info!("mounting {name}");
+
+ unblock(move || {
+ mount(
+ Some("none"),
+ &path,
+ Some(name),
+ MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID,
+ Some(""),
+ )
+ })
+ .await
+ .with_context(move || format!("failed to mount {name}"))?;
+ }
+
+ Ok(())
+}
+
+impl SysFs {
+ async fn mount_sys() -> Result<()> {
+ // check if sysfs exists
+ if !is_fs_available("sysfs").await? {
+ Err(StaticError("failed to mount sysfs")).kind(ErrorKind::Unrecoverable)?;
+ };
+
+ // check if its mounted
+ let path = Path::new("/sys");
+
+ if is_fs_mounted(path).await? {
+ return Ok(());
+ }
+
+ // create /sys if it doesn't exist
+ if async_fs::metadata(path).await.is_err() {
+ async_fs::create_dir(path)
+ .await
+ .context("failed to create /sys")?;
+ }
+
+ info!("mounting /sys");
+
+ // try mount from fstab
+ if try_mount_from_fstab(path).await? {
+ return Ok(());
+ }
+
+ // mount with sysfs fs
+ unblock(move || {
+ mount(
+ Some("none"),
+ path,
+ Some("sysfs"),
+ MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID,
+ Some(""),
+ )
+ })
+ .await
+ .context("failed to mount sysfs")?;
+
+ Ok(())
+ }
+
+ async fn mount_misc() -> Result<()> {
+ join_all([
+ mount_misc_fs("/sys/kernel/security", "securityfs"),
+ mount_misc_fs("/sys/kernel/debug", "debugfs"),
+ mount_misc_fs("/sys/kernel/config", "configfs"),
+ mount_misc_fs("/sys/fs/fuse/connections", "fusectl"),
+ mount_misc_fs("/sys/fs/pstore", "pstore"),
+ ])
+ .await
+ .into_iter()
+ .collect::<Result<Vec<_>>>()?;
+
+ if Path::new("/sys/firmware/efi/efivars").exists()
+ && mount_misc_fs("/sys/firmware/efi/efivars", "efivarfs")
+ .await
+ .is_err()
+ {
+ // efivarfs can be disabled in kernel parameters
+ warn!("failed to mount efivarfs");
+ }
+
+ // TODO; SELinux
+
+ Ok(())
+ }
+}
+
+#[async_trait]
+impl Unit for SysFs {
+ unit_name!("sysfs");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new().need(ProcFs.name()).clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ Self::mount_sys().await?;
+ Self::mount_misc().await?;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/services/getty.rs b/crates/units/src/services/getty.rs
@@ -0,0 +1,60 @@
+use std::sync::OnceLock;
+
+use async_trait::async_trait;
+use blocking::unblock;
+use nix::sys::signal::{Signal, kill};
+use nix::unistd::Pid;
+
+use kanit_common::error::Result;
+use kanit_supervisor::{RestartPolicy, SupervisorBuilder};
+use kanit_unit::{Unit, UnitName};
+
+pub struct GeTTY {
+ name: String,
+ pid: u32,
+ tty: &'static str,
+ serial: bool,
+}
+
+impl GeTTY {
+ pub fn new(tty: &'static str, serial: bool) -> Self {
+ Self {
+ name: format!("getty@{tty}"),
+ pid: 0,
+ tty,
+ serial,
+ }
+ }
+}
+
+#[async_trait]
+impl Unit for GeTTY {
+ fn name(&self) -> UnitName {
+ static NAME: OnceLock<UnitName> = OnceLock::new();
+
+ NAME.get_or_init(|| UnitName::from(self.name.clone()))
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ let child = if self.serial {
+ SupervisorBuilder::new("getty", ["-L", "0", self.tty, "vt100"])
+ } else {
+ SupervisorBuilder::new("getty", ["38400", self.tty])
+ }
+ .restart_policy(RestartPolicy::Always)
+ .restart_delay(2)
+ .spawn()?;
+
+ self.pid = child.id();
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ let pid = self.pid;
+ let _ = unblock(move || kill(Pid::from_raw(pid as i32), Signal::SIGKILL)).await;
+
+ Ok(())
+ }
+}
diff --git a/crates/units/src/services/mod.rs b/crates/units/src/services/mod.rs
@@ -0,0 +1,5 @@
+pub use getty::GeTTY;
+pub use syslog::Syslog;
+
+mod getty;
+mod syslog;
diff --git a/crates/units/src/services/syslog.rs b/crates/units/src/services/syslog.rs
@@ -0,0 +1,55 @@
+use async_trait::async_trait;
+use blocking::unblock;
+use log::info;
+use nix::sys::signal::{Signal, kill};
+use nix::unistd::Pid;
+
+use kanit_common::error::Result;
+use kanit_supervisor::{RestartPolicy, SupervisorBuilder};
+use kanit_unit::{Dependencies, Unit};
+
+use crate::oneshot::{Clock, Hostname, LocalMount};
+use crate::unit_name;
+
+pub struct Syslog {
+ pid: u32,
+}
+
+impl Syslog {
+ #[allow(clippy::new_without_default)]
+ pub fn new() -> Self {
+ Self { pid: 0 }
+ }
+}
+
+#[async_trait]
+impl Unit for Syslog {
+ unit_name!("syslog");
+
+ fn dependencies(&self) -> Dependencies {
+ Dependencies::new()
+ .need(Clock.name())
+ .need(Hostname.name())
+ .need(LocalMount.name())
+ .clone()
+ }
+
+ async fn start(&mut self) -> Result<()> {
+ info!("starting syslog");
+
+ let child = SupervisorBuilder::new("syslogd", [])
+ .restart_policy(RestartPolicy::OnFailure)
+ .spawn()?;
+
+ self.pid = child.id();
+
+ Ok(())
+ }
+
+ async fn stop(&mut self) -> Result<()> {
+ let pid = self.pid;
+ let _ = unblock(move || kill(Pid::from_raw(pid as i32), Signal::SIGKILL)).await;
+
+ Ok(())
+ }
+}
diff --git a/docs/async.md b/docs/async.md
@@ -0,0 +1,17 @@
+# if or if not to async
+
+Async must be used in 2 spots:
+* Units
+* Event loop
+
+Any code that is called in these spots should be marked as async. If no code is executing concurrently, blocking may be
+done in an async block.
+
+An example would be in startup's initialization of the loader. `Loader::initialize` does call a blocking function
+(`std::fs::read`) although as no code is running concurrently, it is allowed.
+
+It is safe to use `SendWrapper` as the async executor is designated as a local executor (running in a single thread).
+Due to the init's job being primarily IO-bound, multithreading only introduces slowdowns with the overhead of locking.
+
+Locks should only be used where required in cases where a `RefCell` might be held over an `await` point. `Loader`
+contains an `ev_lock` which should be held during the event loop to ensure unit references are only held at one point.
diff --git a/docs/man/kanit-unit.5 b/docs/man/kanit-unit.5
@@ -0,0 +1,157 @@
+.\" kanit.unit.5 - man page for Kanit unit file format
+.\" SPDX-License-Identifier: CC0-1.0
+.TH KANIT.UNIT 5 "August 2025" "kanit 0.1.0" "File Formats and Conventions"
+.SH NAME
+kanit.unit \- unit file format for the Kanit init system
+.SH SYNOPSIS
+/etc/kanit/system/\fIunit\fR
+.SH DESCRIPTION
+The
+.B kanit.unit
+file format describes service units for the \fBkanit\fR(8) init system. Unit files define how services are launched, supervised, and related to each other.
+
+Each unit file is a plain-text configuration file, located under \fB/etc/kanit/system/\fIunit\fR\fR where \fIunit\fR is the name of the unit.
+
+.SH SYNTAX
+Unit files use an INI-style format, with simple
+.I key = value
+assignments. Keys and values are parsed literally; quoting may be used for arguments that contain spaces or special characters.
+
+Comments begin with a
+.B #
+character and continue to the end of the line.
+
+.SH FIELDS
+.SS Top-level fields
+.TP
+.B description
+A human-readable summary of the unit's purpose.
+
+.TP
+.B kind
+Specifies the unit type.
+Valid values are:
+.BR oneshot ,
+.BR daemon ,
+or
+.BR builtin .
+
+.TP
+.B pwd
+Working directory for the process.
+
+.TP
+.B root
+Root directory to chroot into before execution.
+
+.TP
+.B group
+Group identity to run as. Can be a numeric GID or group name.
+
+.TP
+.B user
+User identity to run as. Can be a numeric UID or username.
+
+.TP
+.B stdout
+Redirect standard output to a specified file.
+
+.TP
+.B stderr
+Redirect standard error to a specified file.
+
+.TP
+.B cmd
+Command to execute, with optional quoted or escaped arguments. Example:
+.RS
+.nf
+cmd = /bin/echo "Hello world" 'escaped\\nnewline'
+.fi
+.RE
+
+.SS [depends]
+Dependency relationships between units.
+
+.TP
+.B before
+List of units that this unit should start before.
+
+.TP
+.B after
+List of units that this unit should start after.
+
+.TP
+.B needs
+Hard dependencies. These units must be started successfully.
+
+.TP
+.B uses
+Soft dependencies. These units will be started if available, but do not fail the current unit on error.
+
+.TP
+.B wants
+Weak dependencies. These units may be started alongside, but are not required.
+
+.SS [environment]
+Environment variables to set for the process.
+.TP
+Each line sets a
+.B VAR = VALUE
+pair.
+
+.SS [restart]
+Restart behavior configuration.
+
+.TP
+.B delay
+Seconds to wait between restart attempts.
+
+.TP
+.B attempts
+Maximum number of restart attempts before giving up.
+
+.TP
+.B policy
+Restart policy: one of
+.BR never ,
+.BR always ,
+.BR on-success ,
+or
+.BR on-failure .
+
+.SH FILES
+.TP
+.B /etc/kanit/system/*.unit
+Unit definition files.
+
+.SH EXAMPLE
+.EX
+# simple daemon
+description = Example service
+kind = daemon
+
+cmd = /usr/bin/exampled
+
+[depends]
+after = :net
+needs = logger
+
+[environment]
+ENV_MODE = production
+
+[restart]
+policy = on-failure
+delay = 2
+attempts = 5
+.EE
+
+.SH AUTHOR
+Sylvia Ivory <git@sivory.net>
+
+.SH ISSUES
+Please report bugs or issues at:
+.UR mailto:git@sivory.net
+.UE
+
+.SH SEE ALSO
+.BR kanit (8)
diff --git a/docs/man/kanit.8 b/docs/man/kanit.8
@@ -0,0 +1,118 @@
+.TH KANIT 8 "2025-08-03" "kanit 0.1.0" "System Management Commands"
+
+.SH NAME
+kanit \- minimal init system and service manager
+
+.SH SYNOPSIS
+.B kanit
+.RI [ options ] " subcommand " [ arguments ]
+
+.SH DESCRIPTION
+\fBkanit\fR is a minimal, single-binary init daemon and service manager
+
+.SH OPTIONS
+.TP
+.BR \-h , " --help"
+Display help information and exit.
+
+.SH SUBCOMMANDS
+
+.SS Power Commands
+
+.TP
+.B kanit poweroff
+Teardown and power off the system.
+.RS
+.TP
+.BR \-f , " --force"
+Skip teardown and forcibly power off.
+.RE
+
+.TP
+.B kanit reboot
+Teardown and reboot the system.
+.RS
+.TP
+.BR \-f , " --force"
+Skip teardown and forcibly reboot.
+.RE
+
+.TP
+.B kanit halt
+Teardown and halt the system.
+.RS
+.TP
+.BR \-f , " --force"
+Skip teardown and forcibly halt.
+.RE
+
+.TP
+.B kanit kexec
+Teardown and reboot via kexec.
+.RS
+.TP
+.BR \-f , " --force"
+Force kexec reboot without teardown.
+.RE
+
+.SS Diagnostic Commands
+
+.TP
+.B kanit blame
+Print unit startup times.
+.RS
+.TP
+.BR \-s , " --sorted"
+Sort by startup duration.
+.RE
+
+.SS Service Commands
+
+.TP
+.B kanit service
+General service management utility.
+
+.TP
+.B kanit service enable
+Enable a unit at the specified runlevel.
+.RS
+.TP
+.I <unit>
+The name of the unit to enable.
+.TP
+.I [runlevel]
+Optional runlevel. If omitted, uses the default.
+.RE
+
+.TP
+.B kanit service disable
+Disable a unit at the specified runlevel.
+.RS
+.TP
+.I <unit>
+The name of the unit to disable.
+.TP
+.I [runlevel]
+Optional runlevel. If omitted, disables from default.
+.RE
+
+.TP
+.B kanit service list
+List enabled units.
+.RS
+.TP
+.BR \-p , " --plan"
+Show service groups and plan.
+.RE
+
+.SH AUTHOR
+Sylvia Ivory <git@sivory.net>
+
+.SH ISSUES
+Please report bugs or issues at:
+.UR mailto:git@sivory.net
+.UE
+
+.SH SEE ALSO
+.BR kanit-unit (5)
+
diff --git a/docs/unit.md b/docs/unit.md
@@ -0,0 +1,31 @@
+# Unit fields
+
+```ini
+# comments
+description = lorem ipsum
+kind = oneshot | daemon | builtin
+
+pwd = /directory
+root = /directory
+group = gid | group name
+user = uid | user name
+stdout = /file
+stderr = /file
+
+cmd = /path/to/exec "argument" 'with \n escapes' and-unquoted
+
+[depends]
+before = dep1,dep2,dep3
+after = ...
+needs = ...
+uses = ...
+wants = ...
+
+[environment]
+VAR = VALUE
+
+[restart]
+delay = 5
+attempts = 3
+policy = never | always | on-success | on-failure
+```
+\ No newline at end of file
diff --git a/justfile b/justfile
@@ -0,0 +1,25 @@
+debug:
+ cargo build --package kanit-multicall
+ just boot-vm debug
+
+release:
+ cargo build --release --package kanit-multicall
+ just boot-vm release
+
+min:
+ cargo +nightly build -Z build-std=std,panic_abort -Z \
+ build-std-features=panic_immediate_abort --profile min --package kanit-multicall
+ just boot-vm min
+
+test:
+ cargo test --all
+ cargo build --package kanit-multicall --features testing
+ just boot-vm debug
+
+clean:
+ cargo clean
+ rm -rf rootfs alpine.qcow2 initramfs vmlinuz-virt
+
+boot-vm target:
+ [ -f "alpine.qcow2" ] || {{justfile_directory()}}/scripts/prepare-vm
+ {{justfile_directory()}}/scripts/start {{target}}
diff --git a/readme.md b/readme.md
@@ -0,0 +1,82 @@
+# Kanit
+
+Toy init system
+
+## Testing
+
+To test Kanit, the following dependencies are needed:
+* `qemu` (`qemu-system-x86_64`)
+* `expect`
+* `curl`
+* `rust`
+* `just`
+
+Once all dependencies are installed, Kanit can be tested in a vm by running `just`.
+The vm's password is set to nothing by default.
+
+## Controller
+
+Kanit can be controlled with `kanit`.
+
+### Units
+
+Units are written in TOML and can be loaded for next boot with `kanit service enable <unit> [level]`.
+They must be stored at `/etc/kanit` to be found by `kanit`. Units are only ran at boot.
+
+Units can be disabled at next boot with `kanit service disable <unit> [level]` and all enabled units can be
+displayed with `kanit service list`.
+
+### Blame
+
+The time each unit takes to run can be viewed with `kanit blame` (or sorted with `kanit blame -s`).
+
+### Power
+
+The system can be powered off, rebooted, halted, and rebooted with kexec with `kanit <op>` with an
+optional `-f` flag to bypass the init. In addition, the standard `poweroff`, `reboot`, and `halt`
+commands can be used.
+
+## Goal
+
+Kanit aims to be minimal with the ability to be rolled into a single executable (multicall).
+
+## Todo
+
+(In no particular order)
+
+* [x] Fix compiling with `x86_64-unknown-linux-gnu`
+* [ ] Service supervision
+ * [x] `kanit-supervisor`
+ * [ ] Avoid spawning new process and integrate directly into `init`
+ * [ ] Record logs
+* [ ] Dynamically loading units
+ * [ ] Move `kanit-rc/services/*` to unit files instead
+ * [ ] Allow unit files to be baked into the init
+ * [x] Allow unit files to be loaded at startup
+* [ ] (Re)loading units
+* [ ] Configuration
+* [x] Enabled units list
+* [x] Concurrency
+* [ ] More graceful fail over
+ * [x] Emergency shell
+ * [x] Allow for continuing when unit fails to start
+ * [ ] Allow for panic recovery (unable to do with `panic = "abort"`)
+* [x] Split `kanit-rc/src/init` to separate crate
+* [ ] Consider moving `kanit-rc/init/*` to unit files
+* [ ] `*dev`
+ * [x] `mdev`
+ * [ ] `udev`
+* [ ] Testing
+ * [x] Smoke test
+ * [ ] Unit tests
+* [ ] Syslog
+ * [x] Busybox
+ * [ ] Custom
+* [ ] Logging
+ * [x] Startup logger
+ * [ ] Runtime logger
+* [x] Shutdown
+
+## Credit
+
+* OpenRC/Alpine for init scripts
diff --git a/scripts/answers b/scripts/answers
@@ -0,0 +1,19 @@
+KEYMAPOPTS="us us"
+HOSTNAMEOPTS=alpine
+DEVDOPTS=mdev
+INTERFACESOPTS="auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet dhcp
+hostname alpine-test
+"
+TIMEZONEOPTS="UTC"
+PROXYOPTS=none
+APKREPOSOPTS="-1"
+USEROPTS=none
+SSHDOPTS=none
+NTPOPTS="openntpd"
+DISKOPTS="-m sys /dev/sda"
+LBUOPTS=none
+APKCACHEOPTS=none
diff --git a/scripts/controller b/scripts/controller
@@ -0,0 +1,75 @@
+#!/usr/bin/env lexpect
+
+local function read_file(name)
+ local f, err = io.open(name, 'r')
+ if not f then
+ error(err)
+ end
+
+ local contents = f:read('*a')
+
+ f:close()
+
+ return contents
+end
+
+local function file_exists(name)
+ assert(name, 'expected file name path')
+ local f = io.open(name, 'r')
+
+ if f then
+ f:close()
+ return true
+ end
+
+ return false
+end
+
+local args = {
+ '-m',
+ '512',
+ '-nic',
+ 'user',
+ '-boot',
+ 'd',
+ '-cdrom',
+ assert(file_exists(arg[1]), 'expected image path to exist') and arg[1],
+ '-hda',
+ 'alpine.qcow2',
+ '-nographic',
+ '-virtfs',
+ 'local,path=./tmp.share,mount_tag=host0,security_model=mapped,id=host0,multidevs=remap',
+}
+
+if file_exists('/dev/kvm') then
+ table.insert(args, '-enable-kvm')
+end
+
+---
+
+local lexpect = require('lexpect')
+
+local session = lexpect.spawn_args('qemu-system-x86_64', args, {
+ passthrough = true
+})
+
+local function command(cmd)
+ session:exp_string('# ')
+ session:send_line(cmd)
+end
+
+session:exp_string('login: ')
+session:send_line('root')
+
+command('echo \'' .. read_file('./initializer') .. '\' > initializer')
+command('chmod +x ./initializer')
+command('./initializer prep')
+command('setup-alpine -f answers -e')
+
+session:exp_string('[n] ')
+session:send_line('y')
+
+command('./initializer initramfs')
+command('./initializer teardown')
+
+session:exp_eof()
diff --git a/scripts/init b/scripts/init
@@ -0,0 +1,31 @@
+#!/bin/sh
+export PATH="$PATH:/usr/bin:/bin:/usr/sbin:/sbin"
+
+/bin/busybox mkdir -p /usr/bin /usr/sbin
+/bin/busybox --install -s
+
+mount -n -t proc proc /proc
+mount -n -t sysfs sysfs /sys
+mount -n -t devtmpfs devtmpfs /dev ||
+ mount -n -t tmpfs tmpfs /dev
+
+ln -sf /bin/kmod /bin/modprobe
+
+modprobe -a sd_mod virtio ext4 9p overlay
+
+# heh bottoms
+mkdir /top /9p-bottom /qcow2-bottom
+
+mount -t 9p -o trans=virtio,version=9p2000.L,msize=512000,cache=loose,posixacl host0 /9p-bottom
+mount /dev/sda3 /qcow2-bottom
+mount -t tmpfs none /top
+
+mkdir -p /top/work /top/root
+
+mount -t overlay overlay -o ro,lowerdir=/9p-bottom:/qcow2-bottom,upperdir=/top/root,workdir=/top/work /newroot
+
+mount --move /proc /newroot/proc
+mount --move /sys /newroot/sys
+mount --move /dev /newroot/dev
+
+exec switch_root -c /dev/ttyS0 /newroot /sbin/init
diff --git a/scripts/initializer b/scripts/initializer
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+case $1 in
+ prep)
+ mkdir /tmp/host0
+ mount -t 9p -o trans=virtio host0 /tmp/host0
+ cp /tmp/host0/* .
+ ;;
+ teardown)
+ umount /tmp/host0
+ sync
+ poweroff
+ ;;
+ initramfs)
+ mkinitfs -i ./init -o /tmp/host0/initramfs -F "base scsi virtio ext4 9p"
+ cp /media/cdrom/boot/vmlinuz-virt /tmp/host0/
+ ;;
+esac
diff --git a/scripts/prepare-vm b/scripts/prepare-vm
@@ -0,0 +1,53 @@
+#!/bin/sh
+set -eE
+
+trap 'cleanup' ERR
+
+# Initializes an Alpine Linux vm for testing
+IMAGE_URL="https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-virt-3.20.1-x86_64.iso"
+IMAGE=${IMAGE_URL##*/}
+
+###
+
+former_pwd=$(pwd)
+cd -P -- "$(dirname "$0")" || return
+
+download_image() {
+ echo "downloading alpine image"
+
+ [ ! -f "$IMAGE" ] && curl -o "$IMAGE" $IMAGE_URL
+ [ ! -f "$IMAGE.sha256" ] && curl -o "$IMAGE.sha256" "$IMAGE_URL.sha256"
+
+ sha256sum -c "$IMAGE.sha256"
+}
+
+create_vm() {
+ echo "initializing alpine vm"
+
+ [ ! -f "alpine.qcow2" ] && qemu-img create -f qcow2 alpine.qcow2 8G
+
+ mkdir ./tmp.share
+
+ cp ./init ./answers ./tmp.share
+
+ ./controller $IMAGE
+
+ if [ -f "alpine.qcow2" ]; then
+ echo "generated alpine.qcow2"
+ mv ./alpine.qcow2 ./tmp.share/initramfs ./tmp.share/vmlinuz-virt "$former_pwd"
+ fi
+}
+
+cleanup() {
+ echo "cleaning up"
+
+ rm -fv "$IMAGE" "$IMAGE.sha256" ./alpine.qcow2
+
+ if [ -d ./tmp.share ]; then
+ rm -rfv ./tmp.share
+ fi
+}
+
+download_image
+create_vm
+cleanup
diff --git a/scripts/start b/scripts/start
@@ -0,0 +1,30 @@
+#!/bin/sh
+set -ex
+
+MULTICALL="target/x86_64-unknown-linux-musl/$1/kanit-multicall"
+
+# clean rootfs
+rm -rf rootfs
+
+mkdir -p rootfs/sbin rootfs/etc/kanit/enabled
+mkdir -p rootfs/sbin rootfs/etc/kanit/system
+
+mkdir -p rootfs/etc/kanit/enabled/boot
+mkdir -p rootfs/etc/kanit/enabled/default
+
+cp units/* rootfs/etc/kanit/system
+
+# symlinks no work
+touch rootfs/etc/kanit/enabled/boot/syslog
+# getty is the devil
+# touch rootfs/etc/kanit/enabled/default/{getty@tty1,getty@ttyS0}
+
+cp "$MULTICALL" rootfs/sbin/init
+cp "$MULTICALL" rootfs/sbin/kanit
+cp "$MULTICALL" rootfs/sbin/kanit-supervisor
+
+[[ -e "/dev/kvm" ]] && KVM=-enable-kvm || KVM=""
+
+qemu-system-x86_64 -m 512 -nic user -kernel ./vmlinuz-virt -initrd ./initramfs -hda ./alpine.qcow2 \
+ -virtfs local,path=./rootfs,mount_tag=host0,security_model=mapped,id=host0,multidevs=remap -nographic $KVM \
+ -append "quiet console=ttyS0"
diff --git a/src/main.rs b/src/main.rs
@@ -0,0 +1,29 @@
+// me when the binary is plural
+
+use std::env::args;
+use std::path::Path;
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+ let name = args().next().expect("first arg");
+ let name_as_path = Path::new(&name);
+ let bin_name = name_as_path.file_name().expect("file name").to_str();
+
+ match bin_name {
+ Some("init") => kanit_init::handle_cli(),
+ #[cfg(feature = "cli")]
+ Some("kanit") => kanit_cli::handle_cli(true),
+ #[cfg(feature = "cli")]
+ Some("poweroff") => kanit_cli::handle_cli(false),
+ #[cfg(feature = "cli")]
+ Some("reboot") => kanit_cli::handle_cli(false),
+ #[cfg(feature = "cli")]
+ Some("halt") => kanit_cli::handle_cli(false),
+
+ Some("kanit-supervisor") => kanit_supervisor::handle_cli(),
+ _ => {
+ eprintln!("was unable to locate `{}`", bin_name.unwrap_or(""));
+ ExitCode::FAILURE
+ }
+ }
+}
diff --git a/units/syslog b/units/syslog
@@ -0,0 +1,5 @@
+cmd = syslogd
+kind = daemon
+
+[restart]
+policy = on-failure