From ad731f17936f02f639a920d8eed5508ebaf4ac65 Mon Sep 17 00:00:00 2001 From: Mutzi Date: Sat, 27 Jan 2024 14:19:37 +0100 Subject: [PATCH] Initial commit --- .gitignore | 93 ++++++ .idea/.gitignore | 8 + .idea/vcs.xml | 6 + Cargo.lock | 771 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 21 ++ src/config.rs | 68 +++++ src/html.rs | 130 ++++++++ src/main.rs | 132 ++++++++ src/service.rs | 177 +++++++++++ src/watch.rs | 152 ++++++++++ src/web.rs | 89 ++++++ 11 files changed, 1647 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/vcs.xml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/config.rs create mode 100644 src/html.rs create mode 100644 src/main.rs create mode 100644 src/service.rs create mode 100644 src/watch.rs create mode 100644 src/web.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..593cee8 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f86168c --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..867aabf --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4213e02 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,68 @@ +use graph_cycles::Cycles; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub service: Vec, + + #[serde(default)] + pub autostart: Vec +} + +#[derive(Debug, Deserialize)] +pub struct CService { + pub name: String, + pub command: String, + pub directory: Option, + + #[serde(default)] + pub depends_on: Vec +} + +impl Config { + pub fn load() -> Result { + let data = std::fs::read_to_string("minitd.toml") + .map_err(|err| format!("Failed to read config file: {err}"))?; + let config = toml::from_str::(&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) + } +} diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..a4d7d7b --- /dev/null +++ b/src/html.rs @@ -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, "{}{}", state, &service.config.name, desc).ok()?; + if can_start { write!(stream, "Start", &service.config.name).ok()?; } + stream.write_all(b"").ok()?; + if can_stop { write!(stream, "Stop", &service.config.name).ok()?; } + write!(stream, "Tail log", &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"Minitd").ok()?; + stream.write_all(b"").ok()?; + stream.write_all(b"").ok()?; + for (_, service) in &crate::Data::get().services { + index_write_service(&mut stream, service)?; + } + stream.write_all(b"
StateNameDescriptionAction
").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"Minitd
").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::>();
+    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(())
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..466f49b
--- /dev/null
+++ b/src/main.rs
@@ -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>,
+    pub services: std::collections::HashMap
+}
+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 = 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> = 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);
+}
diff --git a/src/service.rs b/src/service.rs
new file mode 100644
index 0000000..5eaba3d
--- /dev/null
+++ b/src/service.rs
@@ -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,
+    pub dependents: Vec,
+    pub restart_count: u8,
+    pub config: crate::config::CService
+}
+
+impl Service {
+    fn get_args(&self) -> Result, 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::>())
+    }
+
+    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; }
+    }
+}
diff --git a/src/watch.rs b/src/watch.rs
new file mode 100644
index 0000000..79d4817
--- /dev/null
+++ b/src/watch.rs
@@ -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) {
+    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) {
+    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::>();
+
+        (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);
+    }
+}
\ No newline at end of file
diff --git a/src/web.rs b/src/web.rs
new file mode 100644
index 0000000..8db6f8e
--- /dev/null
+++ b/src/web.rs
@@ -0,0 +1,89 @@
+use std::io::{Write, BufRead};
+use std::net::TcpStream;
+use crate::{Data, html, watch, WorkItem};
+
+static mut WEBSERVER_FD: Option = 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)
+    }
+}