Initial commit

This commit is contained in:
Mutzi 2024-01-27 14:19:37 +01:00
commit ad731f1793
Signed by: root
GPG Key ID: 2437494E09F13876
11 changed files with 1647 additions and 0 deletions

93
.gitignore vendored Normal file
View File

@ -0,0 +1,93 @@
### CLion+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# End of https://www.toptal.com/developers/gitignore/api/clion+iml
/target
/minitd.toml
/*.log

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

771
Cargo.lock generated Normal file
View File

@ -0,0 +1,771 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base-x"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "const_fn"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "daemonize"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e"
dependencies = [
"libc",
]
[[package]]
name = "discard"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "graph-cycles"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6ad932c6dd3cfaf16b66754a42f87bbeefd591530c4b6a8334270a7df3e853"
dependencies = [
"ahash",
"petgraph",
"thiserror",
]
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hhmmss"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a3a7d0916cb01ef108a66108640419767991ea31d11a1c851bed37686a6062"
dependencies = [
"chrono",
"time",
]
[[package]]
name = "iana-time-zone"
version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "minitd"
version = "0.1.0"
dependencies = [
"base64",
"daemonize",
"graph-cycles",
"hhmmss",
"libc",
"nix",
"petgraph",
"serde",
"shlex",
"toml",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "petgraph"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "serde_json"
version = "1.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
dependencies = [
"sha1_smol",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "standback"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
dependencies = [
"version_check",
]
[[package]]
name = "stdweb"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
dependencies = [
"discard",
"rustc_version",
"stdweb-derive",
"stdweb-internal-macros",
"stdweb-internal-runtime",
"wasm-bindgen",
]
[[package]]
name = "stdweb-derive"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn 1.0.109",
]
[[package]]
name = "stdweb-internal-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
dependencies = [
"base-x",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"serde_json",
"sha1",
"syn 1.0.109",
]
[[package]]
name = "stdweb-internal-runtime"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "time"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [
"const_fn",
"libc",
"standback",
"stdweb",
"time-macros",
"version_check",
"winapi",
]
[[package]]
name = "time-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
dependencies = [
"proc-macro-hack",
"time-macros-impl",
]
[[package]]
name = "time-macros-impl"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"standback",
"syn 1.0.109",
]
[[package]]
name = "toml"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[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 = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "minitd"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nix = { version = "0.27.1", features = ["fs", "process", "event", "signal", "poll", "net"] }
libc = "0.2.152"
daemonize = "0.5.0"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8.8"
shlex = "1.3.0"
petgraph = "0.6.4"
graph-cycles = "0.1.0"
base64 = "0.21.7"
hhmmss = "0.1.0"

68
src/config.rs Normal file
View File

@ -0,0 +1,68 @@
use graph_cycles::Cycles;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
pub service: Vec<CService>,
#[serde(default)]
pub autostart: Vec<String>
}
#[derive(Debug, Deserialize)]
pub struct CService {
pub name: String,
pub command: String,
pub directory: Option<String>,
#[serde(default)]
pub depends_on: Vec<String>
}
impl Config {
pub fn load() -> Result<Config, String> {
let data = std::fs::read_to_string("minitd.toml")
.map_err(|err| format!("Failed to read config file: {err}"))?;
let config = toml::from_str::<Config>(&data)
.map_err(|err| format!("Failed to parse config: {err}"))?;
let mut graph = petgraph::Graph::new();
let mut node_map = std::collections::HashMap::new();
for service in &config.service {
let node = graph.add_node(&service.name);
node_map.insert(service.name.clone(), node);
}
for service in &config.service {
let my_id = node_map.get(&service.name).unwrap();
for dep in &service.depends_on {
let other_id = match node_map.get(dep) {
Some(v) => v,
None => return Err(format!("Service '{}' depends on unknown service '{}'", service.name, dep))
};
graph.add_edge(*my_id, *other_id, ());
}
}
let cycles = graph.cycles();
if !cycles.is_empty() {
let mut msg = String::new();
for cycle in cycles {
msg += "Dependency cycle: ";
for node in &cycle {
msg += graph.node_weight(*node).unwrap();
msg += " -> ";
}
msg += graph.node_weight(cycle[0]).unwrap();
msg += "\n";
}
return Err(msg);
}
for auto in &config.autostart {
if config.service.iter().find(|item| item.name == *auto).is_none() {
return Err(format!("autostart: unkown service '{auto}'"));
}
}
Ok(config)
}
}

130
src/html.rs Normal file
View File

@ -0,0 +1,130 @@
use std::io::Write;
use std::net::TcpStream;
use std::os::fd::AsRawFd;
use base64::Engine;
use hhmmss::Hhmmss;
use crate::{Data, service};
fn index_write_service(stream: &mut impl Write, service: &crate::service::Service) -> Option<()> {
let (state, desc, can_start, can_stop) = match &service.status {
crate::service::ServiceStatus::STOPPED => ("stopped", "".to_string(), true, false),
crate::service::ServiceStatus::STARTED(started_at) =>
("started", format!("Pid: {}, uptime: {}", service.pid.as_ref().map_or(-1, |v| v.0.as_raw()), started_at.elapsed().hhmmss()), false, true),
crate::service::ServiceStatus::STOPPING => ("stopping", "".to_string(), false, false),
crate::service::ServiceStatus::FAILED(err) => ("failed", err.clone(), true, false)
};
write!(stream, "<tr><td data-status=\"{}\"></td><td>{}</td><td>{}</td><td>", state, &service.config.name, desc).ok()?;
if can_start { write!(stream, "<a href=\"/start/{}\">Start</a>", &service.config.name).ok()?; }
stream.write_all(b"</td><td>").ok()?;
if can_stop { write!(stream, "<a href=\"/stop/{}\">Stop</a>", &service.config.name).ok()?; }
write!(stream, "</td><td><a href=\"/tail#{}\">Tail log</a></td></tr>", &service.config.name).ok()
}
pub fn send_404(mut stream: TcpStream) -> Option<()> {
stream.write_all(b"HTTP/1.0 404 Not found\r\nContent-Type: text/plain;charset=utf-8\r\n\r\n404 Not found").ok()
}
pub fn send_301(mut stream: TcpStream) -> Option<()> {
stream.write_all(b"HTTP/1.0 302 Found\r\nLocation: /\r\n\r\n").ok()
}
pub fn send_index(stream: TcpStream) -> Option<()> {
let mut stream = std::io::BufWriter::new(stream);
stream.write_all(b"HTTP/1.0 200 OK\r\nContent-Type: text/html;charset=utf-8\r\n\r\n").ok()?;
stream.write_all(b"<!DOCTYPE html><html lang=\"en\"><head><title>Minitd</title><style>
.center-child { display: flex; flex-direction: column; align-items: center; row-gap: 2em; background-color: #aaffaa44; }
.space-child > * { margin: 0 0.75em; }
a, a:link, a:visited { color: #444; }
a:hover { color: #000; }
table { border: 1px solid #999; }
thead { color: #fff; background-color: #999; }
td, th { padding: 0.25em 0.5em; border-bottom: 1px solid #fff; }
tbody > tr:nth-child(even) { background-color: #eee; }
tbody > tr:nth-child(odd) { background-color: #ddd; }
[data-status] { padding: 0.125em 0.25em; }
[data-status]::after { padding: 0.125em 0.25em; }
[data-status='stopped']::after { content: 'Stopped'; background-color: #ef8; }
[data-status='started']::after { content: 'Started'; background-color: #8f8; }
[data-status='stopping']::after { content: 'Stopping'; background-color: #fc8; }
[data-status='failed']::after { content: 'Failed'; background-color: #faa; }
</style></head><body class=\"center-child\">").ok()?;
stream.write_all(b"<div class=\"space-child\"><a href=\".\">Refresh</a><a href=\"/tail#minitd\">Tail log</a><a href=\"/reload\">Reload</a><a href=\"/stop_all\">Stop all</a><a href=\"/shutdown\">Shutdown</a></div>").ok()?;
stream.write_all(b"<table cellspacing=\"0\"><thead><th>State</th><th>Name</th><th>Description</th><th colspan=\"3\">Action</th></thead><tbody>").ok()?;
for (_, service) in &crate::Data::get().services {
index_write_service(&mut stream, service)?;
}
stream.write_all(b"</tbody></table></body></html>").ok()
}
pub fn send_tail_html(stream: TcpStream) -> Option<()> {
let mut stream = std::io::BufWriter::new(stream);
stream.write_all(b"HTTP/1.0 200 OK\r\nContent-Type: text/html;charset=utf-8\r\n\r\n").ok()?;
stream.write_all(b"<!DOCTYPE html><html lang=\"en\"><head><title>Minitd</title></head><body><pre id=\"c\"></pre><script>").ok()?;
stream.write_all(b"const c = document.getElementById('c'); const e = new EventSource('/_tail/' + location.hash.slice(1)); e.onerror = _ => e.close(); e.addEventListener('line', msg => c.innerText += atob(msg.data));").ok()?;
stream.write_all(b"</script></body></html>").ok()
}
pub fn send_tail(mut stream: TcpStream, file: String) -> Option<()> {
let mut b64_buf = [0_u8; 2048];
stream.write_all(b"HTTP/1.0 200 OK\r\ncontent-type: text/event-stream\r\n\r\n").ok()?;
let file = match std::fs::File::open(file) {
Ok(v) => v,
Err(err) => {
let msg = format!("Failed to open file: {err}");
let size = base64::prelude::BASE64_STANDARD.encode_slice(&msg, &mut b64_buf).ok()?;
stream.write_all(b"event:line\ndata:").ok()?;
stream.write_all(&b64_buf[..size]).ok()?;
return stream.write_all(b"\n\n").ok();
}
};
let mut buf = [0_u8; 1024];
let mut fds = [nix::poll::PollFd::new(&file, nix::poll::PollFlags::POLLIN | nix::poll::PollFlags::POLLHUP | nix::poll::PollFlags::POLLERR)];
loop {
let ready = nix::poll::poll(&mut fds, 15000).ok()?;
if ready == 0 { // timeout
stream.write_all(b"event:ka\ndata:\n\n").ok()?;
} else if fds[0].revents().unwrap().contains(nix::poll::PollFlags::POLLIN) {
let size = nix::unistd::read(file.as_raw_fd(), &mut buf).ok()?;
if size > 0 {
let size = base64::prelude::BASE64_STANDARD.encode_slice(&buf[..size], &mut b64_buf).ok()?;
stream.write_all(b"event:line\ndata:").ok()?;
stream.write_all(&b64_buf[..size]).ok()?;
stream.write_all(b"\n\n").ok()?;
}
} else {
break
}
}
None
}
pub fn reload_config() -> Result<(), String> {
let data = Data::get();
let new_config = crate::config::Config::load()?;
let mut removed_services = data.services.keys().cloned().collect::<std::collections::HashSet<_>>();
for service in new_config.service {
match data.services.iter_mut().find(|s| s.1.config.name == service.name) {
Some((id, s)) => { // Replace old service
s.config = service;
removed_services.remove(id);
}
None => { // New service
let s = service::Service::new(service);
data.services.insert(s.id, s);
}
}
}
let queue = data.work_queue.as_ref().unwrap();
for work in removed_services.iter().map(|id| crate::WorkItem::StopService(*id, false)) { queue.send(work).unwrap(); }
for id in removed_services {
let s = data.services.get(&id).unwrap();
while !s.stopped() { std::thread::sleep(std::time::Duration::from_millis(250)); }
data.services.remove(&id);
}
Ok(())
}

132
src/main.rs Normal file
View File

@ -0,0 +1,132 @@
use std::os::unix::prelude::CommandExt;
use std::process::Command;
use crate::web::setup_webserver;
mod config;
mod service;
mod watch;
mod web;
mod html;
#[derive(Debug)]
enum WorkItem {
StartService(u64),
StopService(u64, bool)
}
#[derive(Debug)]
struct Data {
pub epoll: nix::sys::epoll::Epoll,
pub sigfd: nix::sys::signalfd::SignalFd,
pub work_queue: Option<std::sync::mpsc::Sender<WorkItem>>,
pub services: std::collections::HashMap<u64, service::Service>
}
impl Data {
fn new() -> Self {
let epoll = nix::sys::epoll::Epoll::new(nix::sys::epoll::EpollCreateFlags::EPOLL_CLOEXEC).expect("Failed to create epoll");
let mut sigset = nix::sys::signal::SigSet::empty();
sigset.add(nix::sys::signal::SIGTERM);
sigset.add(nix::sys::signal::SIGINT);
sigset.add(nix::sys::signal::SIGCHLD);
nix::sys::signal::sigprocmask(nix::sys::signal::SigmaskHow::SIG_BLOCK, Some(&sigset), None).expect("Failed to set signal mask");
let sigfd = nix::sys::signalfd::SignalFd::with_flags(&sigset, nix::sys::signalfd::SfdFlags::SFD_CLOEXEC).expect("Failed to create signalfd");
epoll.add(&sigfd, nix::sys::epoll::EpollEvent::new(nix::sys::epoll::EpollFlags::EPOLLIN, watch::EPOLL_SIGFD_ID)).expect("Failed to add signalfd to epoll");
Self {
epoll,
sigfd,
work_queue: None,
services: std::collections::HashMap::new()
}
}
pub fn get() -> &'static mut Self {
static mut INSTANCE: Option<Data> = None;
unsafe {
if INSTANCE.is_none() { INSTANCE.replace(Data::new()); }
INSTANCE.as_mut().unwrap()
}
}
pub fn any_running(&self) -> bool {
return self.services.iter().any(|s| !s.1.stopped());
}
pub fn service_update_dep(&mut self) {
let mut id_map = std::collections::HashMap::new();
for (_, service) in &self.services {
id_map.insert(service.config.name.clone(), service.id);
}
let id_map = id_map;
let mut dep_map: std::collections::HashMap<u64, Vec<u64>> = std::collections::HashMap::new();
for (_, service) in &mut self.services {
service.dependencies.clear();
for dep in &service.config.depends_on {
let did = *id_map.get(dep).unwrap();
service.dependencies.push(did);
dep_map.entry(did).or_default().push(service.id);
}
}
for (_, service) in &mut self.services {
service.dependents = dep_map.entry(service.id).or_default().clone();
}
}
}
fn main() {
let (start_as_daemon, config) = {
let mut start_as_daemon = true;
let mut arg_iter = std::env::args().skip(1);
while let Some(arg) = arg_iter.next() {
if arg == "-n" {
start_as_daemon = false;
} else if arg == "-s" {
let new_args = std::env::args().filter(|s| s != "-s");
Command::new("strace").arg("-f").arg("-e").arg("trace=%process,%ipc,/epoll.*").args(new_args).exec();
} else if arg.starts_with('-') {
eprintln!("Invalid switch '{arg}'");
std::process::exit(1);
} else {
eprintln!("Invalid arg '{arg}'");
std::process::exit(1);
}
}
let config = match config::Config::load() {
Ok(v) => v,
Err(e) => { eprintln!("{e}"); std::process::exit(1); }
};
(start_as_daemon, config)
};
if start_as_daemon {
println!("Launching as daemon!");
daemonize::Daemonize::new()
.working_directory(std::env::current_dir().unwrap())
.start()
.expect("Failed to daemonize");
let log_file = nix::fcntl::open(
"minitd.log",
nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT | nix::fcntl::OFlag::O_APPEND,
nix::sys::stat::Mode::S_IWUSR | nix::sys::stat::Mode::S_IRUSR
).expect("Failed to open log file");
nix::unistd::dup2(log_file, 1).expect("Failed to redirect stdout");
nix::unistd::dup2(log_file, 2).expect("Failed to redirect stderr");
nix::unistd::close(log_file).expect("Failed to close log file");
}
{
let data = Data::get();
for service in config.service {
let service = service::Service::new(service);
data.services.insert(service.id, service);
}
data.service_update_dep();
}
setup_webserver();
watch::watch_services(config.autostart);
}

177
src/service.rs Normal file
View File

@ -0,0 +1,177 @@
use std::ffi::CString;
use std::os::fd::{AsRawFd, FromRawFd};
use crate::Data;
#[derive(Debug, PartialEq)]
pub enum ServiceStatus {
STOPPED,
STARTED(std::time::Instant), // Started at
STOPPING,
FAILED(String)
}
#[derive(Debug)]
pub struct Service {
pub id: u64, // > 255
pub pid: Option<(nix::unistd::Pid, std::os::fd::OwnedFd)>,
pub status: ServiceStatus,
pub reaped: std::sync::atomic::AtomicBool,
pub dependencies: Vec<u64>,
pub dependents: Vec<u64>,
pub restart_count: u8,
pub config: crate::config::CService
}
impl Service {
fn get_args(&self) -> Result<Vec<CString>, String> {
let args = match shlex::split(&self.config.command) {
Some(v) => v,
None => return Err("Invalid command".into())
};
if args.is_empty() {
return Err("Command is empty".into());
}
Ok(args.into_iter().map(|s| CString::new(s).unwrap()).collect::<Vec<_>>())
}
pub fn started(&self) -> bool {
match &self.status {
ServiceStatus::STARTED(_) => !self.reaped.load(std::sync::atomic::Ordering::Relaxed),
_ => false
}
}
pub fn stopped(&self) -> bool {
match &self.status {
ServiceStatus::STOPPED | ServiceStatus::FAILED(_) => true,
_ => false
}
}
pub fn can_autostart(&self) -> bool {
return self.restart_count < 3;
}
pub fn new(config: crate::config::CService) -> Self {
static NEXT_SERVICE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(256);
let id = NEXT_SERVICE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Self {
id,
pid: None,
status: ServiceStatus::STOPPED,
reaped: std::sync::atomic::AtomicBool::new(false),
dependents: Vec::new(),
dependencies: Vec::new(),
restart_count: 0,
config
}
}
pub fn start(&mut self) {
if self.started() { return; }
println!("[U...] {}", &self.config.name);
self.restart_count += 1;
self.reaped.store(false, std::sync::atomic::Ordering::Relaxed);
let args = match self.get_args() {
Ok(v) => v,
Err(err) => {
self.status = ServiceStatus::FAILED(err);
println!("[FAIL] {}", &self.config.name);
return;
}
};
let (r, s) = nix::unistd::pipe().unwrap();
let (r, s) = unsafe { (std::os::fd::OwnedFd::from_raw_fd(r), std::os::fd::OwnedFd::from_raw_fd(s)) };
let mut pipe_buf = [0_u8];
match unsafe {nix::unistd::fork()} {
Ok(nix::unistd::ForkResult::Child) => {
drop(r);
let mut sigset = nix::sys::signal::SigSet::empty();
sigset.add(nix::sys::signal::SIGTERM);
sigset.add(nix::sys::signal::SIGINT);
sigset.add(nix::sys::signal::SIGCHLD);
nix::sys::signal::sigprocmask(nix::sys::signal::SigmaskHow::SIG_UNBLOCK, Some(&sigset), None).expect("Failed to set signal mask");
let log_name = self.config.name.clone() + ".log";
let log_file = nix::fcntl::open(
log_name.as_str(),
nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT | nix::fcntl::OFlag::O_APPEND,
nix::sys::stat::Mode::S_IWUSR | nix::sys::stat::Mode::S_IRUSR
).expect("Failed to open log file");
nix::unistd::dup2(log_file, 1).expect("Failed to redirect stdout");
nix::unistd::dup2(log_file, 2).expect("Failed to redirect stderr");
nix::unistd::close(log_file).expect("Failed to close log file");
nix::unistd::close(0).expect("Failed to close stdin");
nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0)).expect("Failed to set pgid");
if let Some(dir) = &self.config.directory { nix::unistd::chdir(dir.as_str()).expect(&format!("Failed to change dir to {dir}")); }
nix::unistd::write(s.as_raw_fd(), &pipe_buf).unwrap();
drop(s);
nix::unistd::execvp(&args[0], args.as_slice()).unwrap();
std::process::exit(0);
}
Ok(nix::unistd::ForkResult::Parent { child }) => {
drop(s);
let pidfd = nix::errno::Errno::result(unsafe { libc::syscall(libc::SYS_pidfd_open, child.as_raw(), 0 as libc::c_int) as libc::c_int }).expect("Failed to open pidfd");
let pidfd = unsafe { std::os::fd::OwnedFd::from_raw_fd(pidfd) };
self.pid.replace((child, pidfd));
let mut fds = [
nix::poll::PollFd::new(&r, nix::poll::PollFlags::POLLIN | nix::poll::PollFlags::POLLHUP | nix::poll::PollFlags::POLLPRI),
nix::poll::PollFd::new(&r, nix::poll::PollFlags::POLLIN | nix::poll::PollFlags::POLLPRI)
];
nix::poll::poll(&mut fds, -1).expect("Failed to poll fds");
if fds[0].any().unwrap_or(false) {
let read = nix::unistd::read(r.as_raw_fd(), &mut pipe_buf).unwrap();
if read == 0 {
self.status = ServiceStatus::FAILED("Check log".into());
println!("[FAIL] {}", &self.config.name);
return;
}
} else {
self.status = ServiceStatus::FAILED("Check log".into());
println!("[FAIL] {}", &self.config.name);
return;
}
self.status = ServiceStatus::STARTED(std::time::Instant::now());
Data::get().epoll.add(&self.pid.as_ref().unwrap().1, nix::sys::epoll::EpollEvent::new(nix::sys::epoll::EpollFlags::EPOLLIN, self.id)).expect("Failed to add epoll");
println!("[ OK ] {}", &self.config.name);
}
Err(err) => panic!("Failed to fork for service: {}", err)
}
}
pub fn stop(&mut self, manual: bool) {
const TERM_WAIT_MS: u128 = 10000;
if self.stopped() { return; }
if let Some((pid, _)) = self.pid.as_ref() {
let pid = *pid;
println!("[D...] {}", &self.config.name);
self.status = ServiceStatus::STOPPING;
let stop_started = std::time::Instant::now();
nix::sys::signal::kill(pid, nix::sys::signal::SIGTERM).expect("Failed to send SIGTERM");
while !self.reaped.load(std::sync::atomic::Ordering::Relaxed) && stop_started.elapsed().as_millis() < TERM_WAIT_MS {
std::thread::sleep(std::time::Duration::from_millis(250));
}
if !self.reaped.load(std::sync::atomic::Ordering::Relaxed) {
nix::sys::signal::kill(pid, nix::sys::signal::SIGKILL).expect("Failed to send SIGKILL");
}
}
println!("[STOP] {}", &self.config.name);
self.status = ServiceStatus::STOPPED;
if manual { self.restart_count = 0; }
}
}

152
src/watch.rs Normal file
View File

@ -0,0 +1,152 @@
use std::collections::VecDeque;
use crate::{Data, WorkItem};
use crate::service::ServiceStatus;
pub const EPOLL_SIGFD_ID: u64 = 0;
pub const EPOLL_WEBSERVER_ID: u64 = 1;
const EPOLL_MAX_EVENTS: usize = 16;
static SHUTDOWN_STARTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn watch_services(autostart: Vec<String>) {
let starter = std::thread::spawn(move || starter_thread(autostart));
let data = Data::get();
let mut events = [nix::sys::epoll::EpollEvent::empty(); EPOLL_MAX_EVENTS];
loop {
let ready = match data.epoll.wait(&mut events, 15000) {
Ok(v) => v,
Err(nix::errno::Errno::EINTR) => 0,
Err(e) => panic!("Epoll wait failed: {:?}", e)
};
for i in 0..ready {
let event = &events[i];
match event.data() {
EPOLL_SIGFD_ID => {
let sig = data.sigfd.read_signal().unwrap().unwrap();
if sig.ssi_signo == libc::SIGCHLD as u32 {
//println!("Got SIGCHLD, ignoring");
} else {
let s = nix::sys::signal::Signal::try_from(sig.ssi_signo as i32).unwrap();
println!("sigfd event, treated as exit command. Signal: {s:#?}");
SHUTDOWN_STARTED.store(true, std::sync::atomic::Ordering::Relaxed);
for (id, _) in &data.services {
data.work_queue.as_ref().unwrap().send(WorkItem::StopService(*id, false)).unwrap();
}
}
}
EPOLL_WEBSERVER_ID => {
crate::web::webserver_accept();
}
id => {
let service = match data.services.get_mut(&id) {
None => { panic!("Got epoll event for id '{}' but no service found\n{:#?}", id, data.services); }
Some(v) => v,
};
let name = &service.config.name;
let pid = service.pid.take().unwrap();
data.epoll.delete(pid.1).expect("Failed to delete from epoll");
let status = nix::sys::wait::waitpid(pid.0, None).expect("waitpid failed");
match status {
nix::sys::wait::WaitStatus::Exited(_, code) => println!("{name} exited with status {code}"),
nix::sys::wait::WaitStatus::Signaled(_, sig, _) => println!("{name} exited with signal {sig}"),
_ => eprintln!("Unknown status {status:#?}")
}
service.reaped.store(true, std::sync::atomic::Ordering::Relaxed);
data.work_queue.as_ref().unwrap().send(WorkItem::StopService(id, false)).unwrap();
}
}
}
if SHUTDOWN_STARTED.load(std::sync::atomic::Ordering::Relaxed) && !data.any_running() {
break;
}
}
Data::get().work_queue.as_ref().unwrap().send(WorkItem::StopService(0, true)).unwrap();
starter.join().unwrap();
}
fn starter_thread(autostart: Vec<String>) {
let data = Data::get();
let (recv, mut todo) = {
let (s,r) = std::sync::mpsc::channel();
data.work_queue.replace(s);
let mut id_map = std::collections::HashMap::new();
for (_, service) in &data.services {
id_map.insert(service.config.name.clone(), service.id);
}
let todo = autostart.into_iter()
.map(|s| WorkItem::StartService(*id_map.get(&s).unwrap()))
.collect::<VecDeque<_>>();
(r, todo)
};
'main_loop: loop {
while let Some(work) = todo.pop_front() {
match work {
WorkItem::StartService(id) => {
if SHUTDOWN_STARTED.load(std::sync::atomic::Ordering::Relaxed) { continue; }
let (missing_deps, dep_fail) = {
let mut missing_deps = Vec::new();
let mut dep_fail = false;
let service = data.services.get(&id).unwrap();
for dep in &service.dependencies {
let dep = data.services.get(dep).unwrap();
if !dep.started() {
if dep.can_autostart() {
missing_deps.push(WorkItem::StartService(dep.id));
} else {
dep_fail = true;
break;
}
}
}
(missing_deps, dep_fail)
};
let service = data.services.get_mut(&id).unwrap();
if dep_fail {
service.status = ServiceStatus::FAILED("Failed to start dependency".into());
service.restart_count = 3;
continue;
} else if !missing_deps.is_empty() {
for dep in missing_deps { todo.push_front(dep); }
todo.push_back(WorkItem::StartService(id));
continue;
}
service.start();
}
WorkItem::StopService(id, manual) => {
let can_stop = {
let mut can_stop = true;
let service = match data.services.get(&id) {
Some(v) => v,
None => continue
};
for dep in &service.dependents {
let dep = data.services.get(dep).unwrap();
if !dep.stopped() {
todo.push_front(WorkItem::StopService(dep.id, manual));
can_stop = false;
}
}
can_stop
};
let service = data.services.get_mut(&id).unwrap();
if !can_stop {
todo.push_back(WorkItem::StopService(id, manual));
continue;
}
service.stop(manual);
}
}
}
let work = recv.recv().unwrap();
match work { WorkItem::StopService(0, _) => break 'main_loop, _ => {} }
todo.push_back(work);
}
}

89
src/web.rs Normal file
View File

@ -0,0 +1,89 @@
use std::io::{Write, BufRead};
use std::net::TcpStream;
use crate::{Data, html, watch, WorkItem};
static mut WEBSERVER_FD: Option<std::net::TcpListener> = None;
pub fn setup_webserver() {
let data = Data::get();
let listener = std::net::TcpListener::bind("0.0.0.0:9001").expect("Failed to bind listener");
listener.set_nonblocking(true).expect("Failed to set non-blocking");
data.epoll.add(&listener, nix::sys::epoll::EpollEvent::new(nix::sys::epoll::EpollFlags::EPOLLIN, watch::EPOLL_WEBSERVER_ID)).expect("Failed to add socket to epoll");
unsafe { WEBSERVER_FD = Some(listener) };
}
pub fn webserver_accept() {
let listener = unsafe { WEBSERVER_FD.as_ref().unwrap() };
if let Ok((stream, _)) = listener.accept() {
std::thread::spawn(move || handle_stream(stream));
}
}
fn handle_stream(mut stream: TcpStream) -> Option<()> {
stream.set_read_timeout(Some(std::time::Duration::new(5, 0))).ok()?;
stream.set_write_timeout(Some(std::time::Duration::new(5, 0))).ok()?;
let reader = std::io::BufReader::new(&mut stream);
let mut lines = reader.lines();
let (method, path) = {
let first = lines.next()?.ok()?;
let (method, first) = first.split_once(' ')?;
let path = first.split_once(' ')?.0;
(method.to_string(), path.to_string())
};
for line in lines {
if line.ok()?.is_empty() { break; }
}
if method != "GET" { return html::send_404(stream); }
if path == "/" {
html::send_index(stream)
} else if path == "/tail" {
html::send_tail_html(stream)
} else if path == "/reload" {
match html::reload_config() {
Ok(()) => html::send_301(stream),
Err(e) => write!(stream, "HTTP/1.0 400 Bad request\r\nContent-Type: text/plain;charset=utf-8\r\n\r\nFailed to reload config:\n{e}").ok()
}
} else if path == "/stop_all" {
let data = Data::get();
for (id, _) in &data.services {
data.work_queue.as_ref().unwrap().send(WorkItem::StopService(*id, false)).unwrap();
}
html::send_301(stream)
} else if path == "/shutdown" {
nix::sys::signal::kill(nix::unistd::getpid(), nix::sys::signal::SIGTERM).ok()?;
html::send_301(stream)
} else if path.starts_with("/_tail/") {
let mut path = path;
drop(path.drain(..7));
if path.contains('.') || path.contains('/') {
html::send_404(stream)
} else {
let path = path + ".log";
html::send_tail(stream, path)
}
} else if path.starts_with("/start/") {
let service = &path[7..];
let data = Data::get();
match data.services.iter().find(|(_, s)| s.config.name == service) {
Some((id, _)) => {
data.work_queue.as_ref().unwrap().send(WorkItem::StartService(*id)).unwrap();
html::send_301(stream)
},
None => html::send_404(stream)
}
} else if path.starts_with("/stop/") {
let service = &path[6..];
let data = Data::get();
match data.services.iter().find(|(_, s)| s.config.name == service) {
Some((id, _)) => {
data.work_queue.as_ref().unwrap().send(WorkItem::StopService(*id, true)).unwrap();
html::send_301(stream)
},
None => html::send_404(stream)
}
} else {
html::send_404(stream)
}
}