Compare commits

...

19 Commits

Author SHA1 Message Date
c3c74bc47c bump version to 0.0.7 2026-06-12 19:02:32 +02:00
8f3de30cd1 server unarchive when starting 2026-06-12 19:02:01 +02:00
b2ab524b7e rename dockermgr to srvmgr 2026-06-12 18:32:52 +02:00
97f3e4c139 archive server 2026-06-12 18:27:55 +02:00
6aa0ab473d keep already started containers, multi threaded async 2026-06-12 12:50:25 +02:00
5125aed0da add setState command, detect crashed/stopped servers periodically 2026-06-12 00:01:23 +02:00
0bbb8b3245 fix multiple packet in single handle_buffer call, set server as stopped when proxy connection failed and container can't be found 2026-06-11 18:33:38 +02:00
5ccac4f3d4 fix buffer out of range on small packets 2026-06-11 16:52:22 +02:00
37afa2c044 fix compilation warn 2026-06-11 15:32:43 +02:00
887a228cf2 nix disable check phase, fix incomplete/failed write in proxy mode, add FML support, rename executable to dmm, image tag specific to servers, config folder from env, 2026-06-11 15:22:29 +02:00
3f840ea284 fix warnings 2026-06-10 23:08:06 +02:00
fe5235a3ca manager loop auto stop servers 2026-06-10 20:51:02 +02:00
7bae17c525 manager loop WIP, add nix flake 2026-06-10 17:20:29 +02:00
35ac5453d9 login to start 2026-06-10 13:19:10 +02:00
17032a1940 fix proxy mode start 2026-06-09 17:45:44 +02:00
408530db00 embed migrations, fix start, add stop 2026-06-09 17:09:07 +02:00
5136e0bdc3 add docker container creation (add broken since sqlite change) 2026-06-09 00:42:50 +02:00
410ca2eeef switched to sqlite 2026-06-06 19:43:56 +02:00
05c69d26ed start switch to sqlite 2026-06-06 18:19:29 +02:00
37 changed files with 1191 additions and 220 deletions

224
Cargo.lock generated
View File

@ -174,6 +174,42 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel 2.5.0",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.4.1",
"futures-lite",
"rustix",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys",
]
[[package]] [[package]]
name = "async-std" name = "async-std"
version = "1.13.2" version = "1.13.2"
@ -302,12 +338,6 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@ -534,13 +564,13 @@ version = "2.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9940fb8467a0a06312218ed384185cb8536aa10d8ec017d0ce7fad2c1bd882d5" checksum = "9940fb8467a0a06312218ed384185cb8536aa10d8ec017d0ce7fad2c1bd882d5"
dependencies = [ dependencies = [
"bitflags", "chrono",
"byteorder",
"diesel_derives", "diesel_derives",
"downcast-rs", "downcast-rs",
"itoa", "libsqlite3-sys",
"pq-sys",
"r2d2", "r2d2",
"sqlite-wasm-rs",
"time",
] ]
[[package]] [[package]]
@ -567,6 +597,17 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "diesel_migrations"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8"
dependencies = [
"diesel",
"migrations_internals",
"migrations_macros",
]
[[package]] [[package]]
name = "diesel_table_macro_syntax" name = "diesel_table_macro_syntax"
version = "0.3.0" version = "0.3.0"
@ -602,15 +643,20 @@ dependencies = [
[[package]] [[package]]
name = "dockermcmgr" name = "dockermcmgr"
version = "0.1.0" version = "0.0.6"
dependencies = [ dependencies = [
"async-process",
"async-std", "async-std",
"bollard", "bollard",
"chrono", "chrono",
"clap", "clap",
"diesel", "diesel",
"diesel-enum", "diesel-enum",
"diesel_migrations",
"futures-util",
"json", "json",
"num-derive",
"num-traits",
"regex", "regex",
"serde", "serde",
"thiserror", "thiserror",
@ -721,6 +767,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@ -845,7 +897,16 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"foldhash", "foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash 0.2.0",
] ]
[[package]] [[package]]
@ -1233,6 +1294,16 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libsqlite3-sys"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"
@ -1278,6 +1349,27 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "migrations_internals"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
dependencies = [
"serde",
"toml",
]
[[package]]
name = "migrations_macros"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703"
dependencies = [
"migrations_internals",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@ -1305,6 +1397,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1435,17 +1538,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
[[package]]
name = "pq-sys"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61"
dependencies = [
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@ -1535,6 +1627,16 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rsqlite-vfs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
dependencies = [
"hashbrown 0.16.1",
"thiserror",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.4" version = "1.1.4"
@ -1635,6 +1737,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1713,6 +1824,18 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "sqlite-wasm-rs"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
dependencies = [
"cc",
"js-sys",
"rsqlite-vfs",
"wasm-bindgen",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@ -1785,11 +1908,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa",
"js-sys", "js-sys",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde_core", "serde_core",
"time-core", "time-core",
"time-macros",
] ]
[[package]] [[package]]
@ -1798,6 +1923,16 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@ -1848,6 +1983,37 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"winnow 0.7.15",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow 1.0.3",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -2151,6 +2317,18 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]]
name = "winnow"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"

View File

@ -1,18 +1,27 @@
[package] [package]
name = "dockermcmgr" name = "dockermcmgr"
version = "0.1.0" version = "0.0.7"
edition = "2024" edition = "2024"
[[bin]]
name = "dmm"
path = "src/main.rs"
[dependencies] [dependencies]
async-process = "2.5.0"
async-std = "1.13.2" async-std = "1.13.2"
bollard = "0.21.0" bollard = "0.21.0"
chrono = "0.4.44" chrono = "0.4.44"
clap = "4.6.1" clap = "4.6.1"
diesel = { version = "2.3.9", features = ["postgres", "r2d2"] } diesel = { version = "2.3.9", features = ["sqlite", "r2d2", "chrono"] }
diesel-enum = "0.2.1" diesel-enum = "0.2.1"
diesel_migrations = "2.3.2"
futures-util = "0.3.32"
json = "0.12.4" json = "0.12.4"
num-derive = "0.4.2"
num-traits = "0.2.19"
regex = "1.12.3" regex = "1.12.3"
serde = "1.0.228" serde = "1.0.228"
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.52.1", features = ["net", "rt", "macros", "signal", "fs"] } tokio = { version = "1.52.1", features = ["net", "rt", "macros", "signal", "fs", "rt-multi-thread"] }
zip = "8.6.0" zip = "8.6.0"

95
flake.lock generated Normal file
View File

@ -0,0 +1,95 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1780930886,
"narHash": "sha256-rppURzHviaQN131F+nLiLdGfcb0uCd9gGP0E5+iw9MI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8c3cede7ddc26bd659d2d383b5610efbd2c7a16e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1770107345,
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"systems": "systems",
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

81
flake.nix Normal file
View File

@ -0,0 +1,81 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
treefmt-nix.url = "github:numtide/treefmt-nix";
flake-compat = {
url = "github:NixOS/flake-compat";
flake = false;
};
};
outputs =
{
nixpkgs,
...
}@inputs:
with builtins;
with nixpkgs.lib;
let
doubles = import inputs.systems;
forAllSystems = genAttrs doubles;
pkgs_gen =
{ ... }@args:
import nixpkgs args;
dmm_gen =
{
pkgs,
tools ? [ ],
specialArgs ? { },
}:
pkgs.rustPlatform.buildRustPackage (
let
src = cleanSource ./.;
in
final:
{
pname = "dmm";
version = "0.0.7";
inherit src;
cargoLock.lockFile = src + "/Cargo.lock";
nativeBuildInputs = tools;
doCheck = false;
# env.COMPRESSION_COMMAND="${pkgs.lrzip}/bin/lrzip";
buildInputs = with pkgs; [
sqlite
lrzip
];
}
// specialArgs
);
in
{
packages = forAllSystems (
system:
let
pkgs = pkgs_gen { inherit system; };
dmm = dmm_gen { inherit pkgs; };
in
{
inherit dmm;
default = dmm;
}
);
formatter = forAllSystems (
system:
let
pkgs = pkgs_gen { inherit system; };
in
inputs.treefmt-nix.lib.mkWrapper pkgs {
programs.nixfmt.enable = true;
}
);
};
}

View File

@ -1,6 +0,0 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -1,36 +0,0 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -1,11 +1,10 @@
CREATE TABLE "servers" ( CREATE TABLE "servers" (
"id" BIGSERIAL PRIMARY KEY, "name" varchar(255) UNIQUE NOT NULL PRIMARY KEY,
"name" varchar(255) UNIQUE NOT NULL,
"last_login" timestamp, "last_login" timestamp,
"container_id" varchar(30), "status" INT2 NOT NULL DEFAULT 0,
"status" int2 NOT NULL DEFAULT 0,
"is_default" bool NOT NULL DEFAULT false, "is_default" bool NOT NULL DEFAULT false,
"redirect_ip" varchar(50) "redirect_ip" varchar(50),
"simple_redirect" bool NOT NULL DEFAULT false
); );
insert into servers (name, last_login, container_id, status, is_default, redirect_ip) values ('potato', now(), null, 1, false, 'play.hypixel.net:25565'); insert into servers (name, last_login, status, is_default, redirect_ip, simple_redirect) values ('potato', datetime(), 4, false, 'play.hypixel.net:25565', true);

View File

@ -10,7 +10,7 @@ pkgs.mkShell {
]; ];
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
COMPRESSION_COMMAND = "${pkgs.lrzip}/bin/lrzip";
shellHook = '' shellHook = ''
[ ! -f .env ] || export $(grep -v '^#' .env | xargs) [ ! -f .env ] || export $(grep -v '^#' .env | xargs)

View File

@ -1,16 +1,16 @@
use chrono::Utc;
use clap::{ArgMatches, Command, arg}; use clap::{ArgMatches, Command, arg};
use crate::cli::env_settings; use crate::cli::env_settings;
use crate::status::ServerStatus; use crate::status::ServerStatus;
use crate::utils::{ validate_name, extract_archive, move_all_elements}; use crate::utils::{ validate_name, extract_archive, move_all_elements};
use diesel::{insert_into, prelude::*}; use diesel::prelude::*;
use crate::{DbPool, models}; use crate::{DbPool, models}; use crate::schema;
use crate::schema;
use tokio::{fs::{File, create_dir}, io::AsyncWriteExt}; use tokio::{fs::{File, create_dir}, io::AsyncWriteExt};
use std::path::Path; use std::path::Path;
use std::time::SystemTime;
pub fn create_subcommand() -> Command { pub fn create_subcommand() -> Command {
let mut arguments = vec![ let mut arguments = vec![
@ -27,21 +27,22 @@ pub fn create_subcommand() -> Command {
pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{ pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{
use schema::servers::dsl::*;
let config = crate::Config::load(); let config = crate::Config::load();
let name = arg.get_one::<String>("NAME").unwrap(); let server_name = arg.get_one::<String>("NAME").unwrap();
if !validate_name(name) { if !validate_name(server_name) {
return Err("Invalid name provided".to_string()); return Err("Invalid name provided".to_string());
} }
let conn = &mut pool.get().unwrap(); let conn = &mut pool.get().unwrap();
if let Ok(_) = schema::servers::table.filter(schema::servers::name.eq(name)).select(models::Servers::as_select()).first(conn) { if let Ok(_) = schema::servers::table.filter(schema::servers::name.eq(server_name)).select(models::Servers::as_select()).first(conn) {
return Err("Server with this name already exists".to_string()) return Err("Server with this name already exists".to_string())
} }
let path = config.config_path + "/" + name + "/"; let path = config.config_path + "/" + server_name + "/";
create_dir(&path).await.map_err(|e| format!("failed to create the config folder, reason : {}", e))?; create_dir(&path).await.map_err(|e| format!("failed to create the config folder, reason : {}", e))?;
@ -57,13 +58,12 @@ pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{
} }
let new_server = models::CreateServer { let new_server = models::CreateServer {
name: name, name: server_name,
last_login: Some(SystemTime::now()), last_login: Some(Utc::now().naive_utc()),
container_id: None,
status: ServerStatus::Stopped, status: ServerStatus::Stopped,
redirect_ip: None redirect_ip: None
}; };
insert_into(schema::servers::dsl::servers).values(&new_server).execute(conn).map_err(|e| format!("Failed to insert in db, error : {:?}", e))?; diesel::insert_into(servers).values(&new_server).execute(conn).map_err(|e| format!("Failed to insert in db, error : {:?}", e))?;
Ok(()) Ok(())
} }

47
src/cli/archive.rs Normal file
View File

@ -0,0 +1,47 @@
use bollard::Docker;
use clap::Arg;
use clap::{Command, ArgMatches};
use diesel::{SelectableHelper};
use diesel::result::Error::NotFound;
use crate::status::ServerStatus;
use crate::{DbPool, schema, models};
use diesel::prelude::*;
use crate::srvmgr::{self, archive_server};
pub fn create_subcommand() -> Command {
Command::new("archive")
.about("archive a server")
.arg(Arg::new("NAME").required(true))
}
pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: &Docker) -> Result<(), String> {
use schema::servers::dsl::*;
let server_name = arguments.get_one::<String>("NAME").unwrap();
let conn = &mut pool.get().unwrap();
let server = match servers.select(models::Servers::as_select()).filter(name.eq(server_name)).first(conn) {
Ok(server) => {server},
Err(NotFound) => { return Err("server not found".to_string()); },
Err(e) => { return Err(format!("Failed to fetch server from db : {}", e)); }
};
if server.status != ServerStatus::Running && server.status != ServerStatus::Stopped {
return Err(format!("invalid current server state : {}", server.status));
}
if server.status == ServerStatus::Running {
println!("Stopping container");
srvmgr::stop_server(&docker, pool, server_name).await?;
println!("container stopped");
}
archive_server(&server, pool).await?;
Ok(())
}

View File

@ -1,8 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use clap::{ Arg, ArgAction, ArgMatches, parser::ValueSource}; use clap::{ Arg, ArgMatches, parser::ValueSource};
pub fn get_args() -> [Arg; 21] { pub fn get_args() -> [Arg; 22] {
[ [
Arg::new("VERSION").short('v').long("version").help("Minecraft version of the server").default_value("LATEST"), Arg::new("VERSION").short('v').long("version").help("Minecraft version of the server").default_value("LATEST"),
Arg::new("MEMORY").short('m').long("memory").help("How much ram does this server have").default_value("4G"), Arg::new("MEMORY").short('m').long("memory").help("How much ram does this server have").default_value("4G"),
@ -26,12 +26,13 @@ pub fn get_args() -> [Arg; 21] {
Arg::new("SPAWN_ANIMALS").long("spawn-animals").help("spawn animals").default_value("true"), Arg::new("SPAWN_ANIMALS").long("spawn-animals").help("spawn animals").default_value("true"),
Arg::new("SPAWN_MONSTERS").long("spawn-monsters").help("spawn monsters").default_value("true"), Arg::new("SPAWN_MONSTERS").long("spawn-monsters").help("spawn monsters").default_value("true"),
Arg::new("SPAWN_NPCS").long("spawn-npcs").help("spawn npcs").default_value("true"), Arg::new("SPAWN_NPCS").long("spawn-npcs").help("spawn npcs").default_value("true"),
Arg::new("IMAGE_TAG").long("image-tag").help("set the tag of the docker image").default_value("latest")
] ]
} }
pub fn args_to_settings(arguments: &ArgMatches, default: bool) -> Vec<(String, String)> { pub fn args_to_settings(arguments: &ArgMatches, default: bool) -> Vec<(String, String)> {
let default_handle = vec!["VERSION", "MEMORY", "MAX_PLAYERS", "SPAWN_PROTECTION", "ONLINE_MODE", "TYPE", "MOTD", "DIFFICULTY", "OPS", "MODE", "ALLOW_FLIGHT", let default_handle = vec!["VERSION", "MEMORY", "MAX_PLAYERS", "SPAWN_PROTECTION", "ONLINE_MODE", "TYPE", "MOTD", "DIFFICULTY", "OPS", "MODE", "ALLOW_FLIGHT",
"ALLOW_NETHER", "HARDCORE", "MAX_TICK_TIME", "PAUSE_WHEN_EMPTY_SECONDS", "PVP", "SPAWN_ANIMALS", "SPAWN_MONSTERS", "SPAWN_NPCS"]; "ALLOW_NETHER", "HARDCORE", "MAX_TICK_TIME", "PAUSE_WHEN_EMPTY_SECONDS", "PVP", "SPAWN_ANIMALS", "SPAWN_MONSTERS", "SPAWN_NPCS", "IMAGE_TAG"];
let mut res = vec![ let mut res = vec![
("EULA".to_string(), "TRUE".to_string()), ("EULA".to_string(), "TRUE".to_string()),
@ -96,5 +97,3 @@ pub fn update_env(arguments: &ArgMatches, prev_env: String) -> String{
settings_to_env(merged) settings_to_env(merged)
} }
// START COMMAND :
// docker run --rm --env-file ./env -u 1000:100 -it --volume $PWD/server:/data itzg/minecraft-server

View File

@ -2,6 +2,10 @@ pub mod add;
pub mod edit; pub mod edit;
pub mod remove; pub mod remove;
pub mod ls; pub mod ls;
pub mod start;
pub mod stop;
pub mod set_state;
pub mod archive;
pub mod env_settings; pub mod env_settings;

View File

@ -1,6 +1,7 @@
use bollard::Docker;
use clap::{Arg, ArgMatches, Command}; use clap::{Arg, ArgMatches, Command};
use crate::{Config, DbPool}; use crate::{Config, DbPool, srvmgr};
use std::fs; use std::fs;
@ -12,15 +13,15 @@ pub fn create_subcommand() -> Command {
.arg(Arg::new("NAME").required(true)) .arg(Arg::new("NAME").required(true))
} }
pub async fn subcommand(arguments: &ArgMatches, pool: DbPool) -> Result<(), String> { pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: &Docker) -> Result<(), String> {
use crate::schema::servers::dsl::*; use crate::schema::servers::dsl::*;
let server_name = arguments.get_one::<String>("NAME").unwrap(); let server_name = arguments.get_one::<String>("NAME").unwrap();
let config = Config::load(); let config = Config::load();
let conn = &mut pool.get().unwrap(); let conn = &mut pool.get().unwrap();
//todo stop the server if it's started let _ = srvmgr::stop_server(docker, pool, server_name).await;
let deleted = diesel::delete(servers.filter(name.eq(server_name))).execute(conn).map_err(|_| "failed to delete from db".to_string())?; let deleted = diesel::delete(servers.filter(name.eq(server_name))).execute(conn).map_err(|_| "failed to delete from db".to_string())?;
if deleted != 1 { if deleted != 1 {

33
src/cli/set_state.rs Normal file
View File

@ -0,0 +1,33 @@
use clap::Arg;
use clap::ArgMatches;
use clap::Command;
use diesel::prelude::*;
use crate::DbPool;
use crate::schema;
use crate::status::ServerStatus;
pub fn create_subcommand() -> Command {
Command::new("setState")
.about("force the known state of a server (will not change the actual state)")
.arg(Arg::new("NAME").required(true))
.arg(Arg::new("STATE").required(true))
}
pub async fn subcommand(arguments: &ArgMatches, pool: DbPool) -> Result<(), String> {
use schema::servers::dsl::*;
let server_name = arguments.get_one::<String>("NAME").unwrap();
let wanted_state = match arguments.get_one::<String>("STATE").unwrap().as_str() {
"stopped" => {ServerStatus::Stopped},
"running" => {ServerStatus::Running},
_ => { return Err("invalid state, must be `stoppped` or `running`".to_string()) }
};
let conn = &mut pool.get().unwrap();
diesel::update(servers).filter(name.eq(server_name)).set(status.eq(wanted_state)).execute(conn).map_err(|e| format!("Failed to set the server to the wanted state : {}", e))?;
Ok(())
}

26
src/cli/start.rs Normal file
View File

@ -0,0 +1,26 @@
use bollard::Docker;
use chrono::Utc;
use clap::Arg;
use clap::{Command, ArgMatches};
use crate::{ DbPool, schema };
use crate::srvmgr;
use diesel::prelude::*;
pub fn create_subcommand() -> Command {
Command::new("start")
.about("manually start a server (and set last login datetime to now)")
.arg(Arg::new("NAME").required(true))
}
pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: &Docker) -> Result<(), String> {
use schema::servers::dsl::*;
let server_name = arguments.get_one::<String>("NAME").unwrap();
srvmgr::start_server(&docker, pool, server_name).await?;
diesel::update(servers).filter(name.eq(server_name)).set(last_login.eq(Utc::now().naive_utc())).execute(&mut pool.get().unwrap()).map_err(|e| format!("Failed to set the last login date: {}", e))?;
Ok(())
}

22
src/cli/stop.rs Normal file
View File

@ -0,0 +1,22 @@
use bollard::Docker;
use clap::Arg;
use clap::{Command, ArgMatches};
use crate::DbPool;
use crate::srvmgr;
pub fn create_subcommand() -> Command {
Command::new("stop")
.about("manually stop a server")
.arg(Arg::new("NAME").required(true))
}
pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: &Docker) -> Result<(), String> {
let server_name = arguments.get_one::<String>("NAME").unwrap();
println!("Stopping container");
srvmgr::stop_server(&docker, pool, server_name).await?;
println!("container stopped");
Ok(())
}

View File

@ -63,23 +63,18 @@ pub async fn extract_archive(archive_path: String, output: String) -> Result<(),
pub fn move_all_elements(src: &Path, dst: &Path) -> io::Result<()> { pub fn move_all_elements(src: &Path, dst: &Path) -> io::Result<()> {
// 1. Ensure the destination directory exists; if not, create it
if !dst.exists() { if !dst.exists() {
fs::create_dir_all(dst)?; fs::create_dir_all(dst)?;
println!("Created destination directory: {:?}", dst); println!("Created destination directory: {:?}", dst);
} }
// 2. Read the contents of the source directory
for entry in fs::read_dir(src)? { for entry in fs::read_dir(src)? {
let entry = entry?; let entry = entry?;
let file_name = entry.file_name(); let file_name = entry.file_name();
// 3. Construct the new path (destination + filename)
let old_path = entry.path(); let old_path = entry.path();
let new_path = dst.join(file_name); let new_path = dst.join(file_name);
// 4. Perform the move
// fs::rename works for both files and directories
fs::rename(&old_path, &new_path)?; fs::rename(&old_path, &new_path)?;
println!("Moved: {:?} -> {:?}", old_path, new_path); println!("Moved: {:?} -> {:?}", old_path, new_path);
} }

View File

@ -1,11 +1,15 @@
use std::env;
pub struct Config { pub struct Config {
pub config_path: String pub config_path: String,
pub docker_image: String,
} }
impl Config { impl Config {
pub fn load() -> Self { pub fn load() -> Self {
Self { Self {
config_path: "/home/tom/desktop/rust/yo_mama/servers".to_string() // that's my program I do what I want config_path: env::var("DMM_CONFIG_PATH").unwrap_or_else(|_| "/raid/mc_servers".to_string()),
docker_image: "itzg/minecraft-server".to_string(),
} }
} }
} }

View File

@ -1,6 +0,0 @@
use bollard::Docker;
use std::sync::LazyLock;
pub static mut DOCKER: LazyLock<Docker> = LazyLock::new(|| Docker::connect_with_local_defaults().expect("Failed to connect to the docker socket") );

View File

@ -6,7 +6,7 @@
/* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */ /* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */ /* +#+#+#+#+#+ +#+ */
/* Created: 2026/05/29 21:22:17 by tomoron #+# #+# */ /* Created: 2026/05/29 21:22:17 by tomoron #+# #+# */
/* Updated: 2026/06/06 15:37:52 by tomoron ### ########.fr */ /* Updated: 2026/06/12 18:32:19 by tomoron ### ########.fr */
/* */ /* */
/* ************************************************************************** */ /* ************************************************************************** */
@ -15,33 +15,37 @@ use minecraft::mc_socket_listen;
use diesel::{prelude::*, r2d2::{ConnectionManager, Pool}}; use diesel::{prelude::*, r2d2::{ConnectionManager, Pool}};
use std::env;
pub mod schema; pub mod schema;
pub mod models; pub mod models;
pub mod status; pub mod status;
pub mod srvmgr;
mod cli; mod cli;
use cli::*; use cli::*;
mod config; mod config;
pub use config::Config; pub use config::Config;
pub type DbPool = Pool<ConnectionManager<PgConnection>>; use clap::Command;
pub type DbPool = Pool<ConnectionManager<SqliteConnection>>;
use bollard::Docker;
mod migrations;
fn get_connection_pool() -> DbPool { fn get_connection_pool() -> DbPool {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = "sqlite://".to_string() + &Config::load().config_path + "/db.sqlite";
let manager = ConnectionManager::<PgConnection>::new(&database_url); let manager = ConnectionManager::<SqliteConnection>::new(&database_url);
Pool::builder() Pool::builder()
.build(manager) .build(manager)
.unwrap_or_else(|_| panic!("Error creating pool for {}", database_url)) .unwrap_or_else(|e| panic!("Error creating pool for {}\nerror: {:?}", database_url, e))
} }
use clap::Command;
fn cli() -> Command { fn cli() -> Command {
Command::new("dmm") Command::new("dmm")
.about("Docker Minecraft server Manager cli") .about("Docker Minecraft server Manager cli")
@ -56,19 +60,32 @@ fn cli() -> Command {
.subcommand(remove::create_subcommand()) .subcommand(remove::create_subcommand())
.subcommand(edit::create_subcommand()) .subcommand(edit::create_subcommand())
.subcommand(ls::create_subcommand()) .subcommand(ls::create_subcommand())
.subcommand(set_state::create_subcommand())
.subcommand(start::create_subcommand())
.subcommand(stop::create_subcommand())
.subcommand(archive::create_subcommand())
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), String> { async fn main() -> Result<(), String> {
let arg = cli().get_matches(); let arg = cli().get_matches();
let pool = get_connection_pool(); let pool = get_connection_pool();
migrations::run_migrations(&pool).map_err(|e| format!("Failed to run migrations : {}", e))?;
let docker = Docker::connect_with_defaults().expect("Failed to connect to local docker");
match arg.subcommand() { match arg.subcommand() {
Some(("serverMode", _)) => { mc_socket_listen(pool).await.map_err(|_| "socket error".to_string())?; }, Some(("serverMode", _)) => { mc_socket_listen(pool, docker).await.map_err(|_| "socket error".to_string())?; },
Some(("add", sub_matches)) => { add::subcommand(sub_matches, pool).await?; }, Some(("add", sub_matches)) => { add::subcommand(sub_matches, pool).await?; },
Some(("remove", sub_matches)) => { remove::subcommand(sub_matches, pool).await?; }, Some(("remove", sub_matches)) => { remove::subcommand(sub_matches, &pool, &docker).await?; },
Some(("ls", sub_matches)) => { ls::subcommand(sub_matches, pool).await?; }, Some(("ls", sub_matches)) => { ls::subcommand(sub_matches, pool).await?; },
Some(("edit", sub_matches)) => { edit::subcommand(sub_matches, pool).await?; }, Some(("edit", sub_matches)) => { edit::subcommand(sub_matches, pool).await?; },
Some(("setState", sub_matches)) => { set_state::subcommand(sub_matches, pool).await? },
Some(("start", sub_matches)) => { start::subcommand(sub_matches, &pool, &docker).await?; },
Some(("stop", sub_matches)) => { stop::subcommand(sub_matches, &pool, &docker).await?; },
Some(("archive", sub_matches)) => { archive::subcommand(sub_matches, &pool, &docker).await? },
_ => {panic!("subcommand not implemented")} _ => {panic!("subcommand not implemented")}
} }
Ok(()) Ok(())

15
src/migrations.rs Normal file
View File

@ -0,0 +1,15 @@
use std::error::Error;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use crate::DbPool;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
pub fn run_migrations(pool: &DbPool) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
let connection = &mut pool.get().unwrap();
connection.run_pending_migrations(MIGRATIONS)?;
Ok(())
}

View File

@ -2,17 +2,22 @@ use crate::minecraft::client::client::Client;
use crate::minecraft::varint::varint_read; use crate::minecraft::varint::varint_read;
use std::cmp::min;
use std::collections::VecDeque;
use std::io; use std::io;
use tokio::io::AsyncWriteExt;
impl Client { impl Client {
pub async fn in_read(&mut self) -> Result<u8, String> { pub async fn in_read(&mut self) -> Result<u8, String> {
loop loop
{ {
match self.in_stream.try_read(&mut self.read_buf) { match self.in_stream.try_read(&mut self.read_buf) {
Ok(n) => { Ok(n) => {
if let Some(out_stream) = &self.out_stream { if let Some(out_stream) = &mut self.out_stream {
let _ = out_stream.writable().await;
let _ = out_stream.try_write(&self.read_buf[..n]); out_stream.write_all(&self.read_buf[..n]).await.map_err(|e| format!("Failed to write to server stream :{}", e))?;
} else { } else {
self.buffer_append((&self.read_buf[..n]).to_vec()).await?; self.buffer_append((&self.read_buf[..n]).to_vec()).await?;
} }
@ -23,8 +28,8 @@ impl Client {
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
return Ok(1); return Ok(1);
} }
Err(_e) => { Err(e) => {
return Err("read failed".to_string()) return Err(format!("in read failed: {}", e))
} }
} }
} }
@ -46,8 +51,7 @@ impl Client {
match self.out_stream.as_mut().unwrap().try_read(&mut self.read_buf) { match self.out_stream.as_mut().unwrap().try_read(&mut self.read_buf) {
Ok(n) => { Ok(n) => {
let _ = self.in_stream.writable().await; self.in_stream.write_all(&self.read_buf[..n]).await.map_err(|e| format!("Failed to write to client stream :{}", e))?;
let _ = self.in_stream.try_write(&self.read_buf[..n]);
if n == 0 { Ok(0) } else { Ok(1) } if n == 0 { Ok(0) } else { Ok(1) }
} }
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
@ -60,10 +64,13 @@ impl Client {
} }
} }
pub async fn buffer_append(&mut self, data: Vec<u8>) -> Result<(),String> { pub async fn buffer_append(&mut self, data: Vec<u8>) -> Result<(),String> {
if data.len() == 0 { if data.len() == 0 {
return Ok(()); return Ok(());
} }
println!("[{}] adding {} bytes to the buffer",self, data.len());
if self.buffer.len() + data.len() > 65536 { if self.buffer.len() + data.len() > 65536 {
@ -72,20 +79,38 @@ impl Client {
let _ = self.buffer.extend(data); let _ = self.buffer.extend(data);
while self.buffer.len() != 0 && self.buffer.len() - 1 >= self.buffer[0] as usize {
loop {
if self.buffer.len() == 0 {
break;
}
let varint_buffer_len = min(self.buffer.len(), 5);
let mut varint_buffer: VecDeque<u8> = self.buffer[..varint_buffer_len].to_vec().into();
let expected_len = varint_read(&mut varint_buffer)?;
let varint_len = varint_buffer_len - varint_buffer.len();
println!("current len : {}, expected_len : {}", self.buffer.len() -varint_len, expected_len);
if self.buffer.len() - varint_len < expected_len as usize {
break ;
}
println!("buffer : {:?}", self.buffer);
if self.buffer[0] == 0 { if self.buffer[0] == 0 {
return Err("invalid packet".to_string()); return Err("invalid packet".to_string());
} }
self.handle_packet(self.buffer[varint_len..=(expected_len as usize)].to_vec().into()).await?;
let len = varint_read(&mut self.buffer.clone().into())? as usize; self.buffer.drain(..(expected_len as usize + varint_len));
self.handle_packet(self.buffer[1..=len].to_vec().into()).await?;
self.buffer.drain(..len + 1);
if let Some(out_stream) = &self.out_stream { if let Some(out_stream) = &self.out_stream {
if self.buffer.len() != 0 { if self.buffer.len() != 0 {
let _ = out_stream.writable().await; let _ = out_stream.writable().await.map_err(|e| format!("out stream writable call failed : {}", e))?;
let _ = out_stream.try_write(self.buffer.as_slice()); let _ = out_stream.try_write(self.buffer.as_slice()).map_err(|e| format!("Failed to send remaining buffer to out stream : {}", e))?;
self.buffer.drain(..); self.buffer.drain(..);
} }
} }

View File

@ -6,16 +6,29 @@
/* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */ /* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */ /* +#+#+#+#+#+ +#+ */
/* Created: 2026/05/07 17:23:09 by tomoron #+# #+# */ /* Created: 2026/05/07 17:23:09 by tomoron #+# #+# */
/* Updated: 2026/06/05 12:36:46 by tomoron ### ########.fr */ /* Updated: 2026/06/11 17:25:09 by tomoron ### ########.fr */
/* */ /* */
/* ************************************************************************** */ /* ************************************************************************** */
use bollard::Docker;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::minecraft::handshake::Handshake; use crate::minecraft::handshake::Handshake;
use crate::status::ServerStatus;
use std::fmt; use std::fmt;
use crate::models::Servers; use crate::models::Servers;
use crate::DbPool;
use crate::schema;
use crate::models;
use diesel::prelude::*;
use regex::Regex;
use chrono::Utc;
pub struct Client { pub struct Client {
pub in_stream: TcpStream, pub in_stream: TcpStream,
@ -28,19 +41,12 @@ pub struct Client {
pub server: Option<Result<Servers,diesel::result::Error>>, pub server: Option<Result<Servers,diesel::result::Error>>,
pub db_pool: DbPool pub db_pool: DbPool,
pub docker: Docker
} }
use crate::DbPool;
use crate::schema;
use crate::models;
use diesel::prelude::*;
use regex::Regex;
impl Client { impl Client {
pub fn create(stream: TcpStream, pool: DbPool) -> Self { pub fn create(stream: TcpStream, pool: DbPool, docker: Docker) -> Self {
Self { Self {
in_stream: stream, in_stream: stream,
@ -53,7 +59,9 @@ impl Client {
server: None, server: None,
db_pool: pool db_pool: pool,
docker: docker
} }
} }
@ -67,10 +75,12 @@ impl Client {
let conn = &mut self.db_pool.get().unwrap(); let conn = &mut self.db_pool.get().unwrap();
let reg = Regex::new(r"^(?:([a-zA-Z-_]*)\.)?mc\.tmoron\.fr$").unwrap(); let reg = Regex::new(r"^(?:([a-zA-Z-_]*)\.)?mc\.tmoron\.fr").unwrap();
let mut found: Option<String> = None; let mut found: Option<String> = None;
if let Some(capture) = reg.captures(&self.handshake.as_ref().unwrap().server_address) { if let Some(capture) = reg.captures(&self.handshake.as_ref().unwrap().server_address) {
println!("capture : {:?}", capture);
if let Some(captured_str) = capture.get(1) { if let Some(captured_str) = capture.get(1) {
println!("found valid capture : {}", captured_str.as_str());
found = Some(captured_str.as_str().to_string()); found = Some(captured_str.as_str().to_string());
} }
} }
@ -85,6 +95,42 @@ impl Client {
} }
pub async fn update_last_login(&mut self) {
use schema::servers::dsl::*;
let server = match self.server.as_ref().unwrap() {
Ok(server) => {server},
Err(_) => { return; },
};
let conn = &mut self.db_pool.get().unwrap();
if self.handshake.as_ref().unwrap().intent == 2 {
match diesel::update(servers).filter(name.eq(&server.name)).set(last_login.eq(Utc::now().naive_utc())).execute(conn) {
Err(e) => { eprintln!("Failed to set last login time : {}",e); },
Ok(_) => {},
};
}
}
pub async fn check_online(&mut self) {
use schema::servers::dsl::*;
let server = match self.server.as_ref().unwrap() {
Ok(server) => { server },
Err(_) => { return; },
};
let conn = &mut self.db_pool.get().unwrap();
match self.docker.inspect_container(&format!("minecraft-{}", server.name), None).await {
Ok(_) => { return; },
Err(_) => {
let _ = diesel::update(servers).filter(name.eq(&server.name)).set(status.eq(ServerStatus::Stopped)).execute(conn);
},
};
}
} }
impl fmt::Display for Client { impl fmt::Display for Client {

View File

@ -1,16 +1,44 @@
use crate::minecraft::client::client::Client; use crate::{srvmgr::start_server, minecraft::client::client::Client};
use diesel::result::Error::NotFound;
use json::object; use json::object;
use crate::minecraft::varint::varint_write; use crate::minecraft::varint::varint_write;
use std::collections::VecDeque; use std::collections::VecDeque;
impl Client { impl Client {
pub async fn login_intent_handle(&mut self, mut _packet: &mut VecDeque<u8>, _packet_id: i32) -> Result<(),String> { pub async fn login_intent_handle(&mut self, mut _packet: &mut VecDeque<u8>, _packet_id: i32) -> Result<(),String> {
match self.login_start_server().await {
Ok(_) => {
self.login_send_disconnect(&"Start request successful, the server will start soon".to_string(), &"green".to_string()).await?;
return Ok(());
}
Err(e) => {
self.login_send_disconnect(&e, &"red".to_string()).await?;
return Err(e);
},
}
}
async fn login_start_server(&mut self) -> Result<(), String> {
let server = match self.server.as_ref().unwrap().as_ref() {
Ok(server) => { server },
Err(NotFound) => { return Err("This server was not found".to_string()); },
Err(e) => { return Err(format!("db fetch error : {}", e)); }
};
start_server(&self.docker, &self.db_pool, &server.name).await?;
Ok(())
}
async fn login_send_disconnect(&mut self, message: &String, color: &String) -> Result<(), String> {
let reason_json = object! { let reason_json = object! {
"type": "text", "type": "text",
"text": "login to start not implemented yet", "text": *message.clone(),
"color": "red" "color": *color.clone()
}; };
let mut response: Vec<u8> = vec![]; let mut response: Vec<u8> = vec![];
let response_json = json::stringify(reason_json); let response_json = json::stringify(reason_json);
let mut response_json_len = varint_write(response_json.len() as i32); let mut response_json_len = varint_write(response_json.len() as i32);

View File

@ -7,8 +7,10 @@ use std::collections::VecDeque;
use tokio::net::TcpStream; use tokio::net::TcpStream;
impl Client { impl Client {
pub async fn handle_packet(&mut self, mut packet: VecDeque<u8>) -> Result<(), String> { pub async fn handle_packet(&mut self, mut packet: VecDeque<u8>) -> Result<(), String> {
println!("packet len : {}", packet.len());
let packet_id = varint_read(&mut packet)?; let packet_id = varint_read(&mut packet)?;
@ -18,6 +20,9 @@ impl Client {
} }
self.handshake = Some(Handshake::from_packet(&mut packet)?); self.handshake = Some(Handshake::from_packet(&mut packet)?);
self.server = Some(self.get_server().await); self.server = Some(self.get_server().await);
self.update_last_login().await;
if let Some(Ok(server)) = &self.server { if let Some(Ok(server)) = &self.server {
if server.status == ServerStatus::Running { if server.status == ServerStatus::Running {
return self.proxy_start().await; return self.proxy_start().await;
@ -56,13 +61,29 @@ impl Client {
async fn proxy_start(&mut self) -> Result<(),String> { async fn proxy_start(&mut self) -> Result<(),String> {
println!("[{}] starting proxy mode", self); println!("[{}] starting proxy mode", self);
// let stream = TcpStream::connect(&self.server.as_ref().unwrap().redirect_ip.as_ref().unwrap()).await let server = &self.server.as_ref().unwrap().as_ref().unwrap();
let stream = TcpStream::connect("play.hypixel.net").await let redirect_ip = server.redirect_ip.as_ref().unwrap();
.map_err(|_| "failed to connect to remote host".to_string())?; let split_ip = redirect_ip.split_once(":");
let domain : String = split_ip.unwrap_or_else(|| (redirect_ip, "")).0.to_string();
let port : u16 = split_ip.unwrap_or_else(|| ("", "25565")).1.parse().map_err(|e| format!("Failed to parse redirect_ip : {}", e))?;
let stream = match TcpStream::connect(redirect_ip).await {
Ok(stream) => {stream},
Err(e) => {
self.check_online().await;
return Err(format!("failed to connect to remote host : {}", e));
},
};
let mut new_handshake = self.handshake.as_ref().unwrap().clone(); let mut new_handshake = self.handshake.as_ref().unwrap().clone();
new_handshake.server_address = "play.hypixel.net".to_string(); if server.simple_redirect {
new_handshake.server_port = 25565; new_handshake.server_address = domain;
} else {
new_handshake.server_address = self.handshake.as_ref().unwrap().server_address.clone();
}
new_handshake.server_port = port;
let handshake_packet = new_handshake.to_packet(); let handshake_packet = new_handshake.to_packet();
let mut packet = Vec::new(); let mut packet = Vec::new();
@ -73,8 +94,12 @@ impl Client {
let _ = self.out_stream.as_ref().unwrap().writable().await; let _ = self.out_stream.as_ref().unwrap().writable().await;
let _ = self.out_stream.as_ref().unwrap().try_write(packet.as_slice()); let _ = self.out_stream.as_ref().unwrap().try_write(packet.as_slice());
println!("[{}] sent handshake to server", self); println!("[{}] sent handshake to server", self);
println!("proxy mode started, buffer len : {}", self.buffer.len());
Ok(()) Ok(())
} }
} }

View File

@ -7,7 +7,7 @@ use json::object;
use crate::minecraft::varint::varint_write; use crate::minecraft::varint::varint_write;
use std::collections::VecDeque; use std::collections::VecDeque;
use chrono::{DateTime, Utc}; use chrono::{DateTime, TimeZone, Utc};
impl Client { impl Client {
pub async fn status_intent_handle(&self, packet: &mut VecDeque<u8>, packet_id: i32) -> Result<(),String> { pub async fn status_intent_handle(&self, packet: &mut VecDeque<u8>, packet_id: i32) -> Result<(),String> {
@ -53,7 +53,7 @@ impl Client {
Ok(server) => { Ok(server) => {
let login = match server.last_login { let login = match server.last_login {
Some(date) => { Some(date) => {
let datetime: DateTime<Utc> = date.into(); let datetime: DateTime<Utc> = Utc.from_utc_datetime(&date);
format!("{}", datetime.format("%d-%m-%Y at %H:%M").to_string()) format!("{}", datetime.format("%d-%m-%Y at %H:%M").to_string())
}, },

View File

@ -0,0 +1,88 @@
use bollard::Docker;
use chrono::{TimeDelta, Utc};
use crate::{ DbPool, models::{self, Servers}, schema, srvmgr::{self, archive_server}, status::ServerStatus };
use async_std::task;
use std::time::Duration;
use diesel::prelude::*;
async fn server_init(pool: &DbPool, _docker: &Docker) -> Result<(), String> {
use schema::servers::dsl::*;
let conn = &mut pool.get().unwrap();
println!("init start");
let _ = diesel::update(servers).filter(
simple_redirect.eq(false)
.and(status.eq(ServerStatus::Stopping)
.or(status.eq(ServerStatus::Starting))
))
.set(status.eq(ServerStatus::Stopped)).execute(conn);
Ok(())
}
pub async fn server_manager_loop(pool: DbPool, docker: Docker)
{
use schema::servers::dsl::*;
let conn = &mut pool.get().unwrap();
match server_init(&pool, &docker).await {
Err(e) => { eprintln!("init failed , ignoring because idgaf : {}", e); },
Ok(_) => {},
};
loop {
let a_servers = match servers.select(models::Servers::as_select()).filter(simple_redirect.eq(false)).load(conn) {
Ok( srv ) => { srv },
Err(error) => { eprintln!("Failed to retreive servers, ignoring : {}", error); vec![] },
};
for srv in a_servers {
let time_diff = Utc::now().naive_utc() - srv.last_login.unwrap();
should_stop(&docker, &pool, &time_diff, &srv).await;
should_archive(&docker, &pool, &time_diff, &srv).await;
check_still_online(&docker, &pool, &srv).await;
}
task::sleep(Duration::from_secs(30)).await;
}
}
async fn should_stop(docker: &Docker, pool: &DbPool, diff: &TimeDelta, srv: &Servers) {
if srv.status != ServerStatus::Running { return ; }
if diff.num_days() >= 7 {
println!("last login time exceeded, stopping {}", srv.name);
let _ = srvmgr::stop_server(docker, pool, &srv.name).await;
println!("{} stopped", srv.name);
}
}
async fn should_archive(_docker: &Docker, pool: &DbPool, diff: &TimeDelta, srv: &Servers) {
if srv.status != ServerStatus::Stopped { return ; }
if diff.num_days() >= 30 {
println!("last login time exceedd, archiving {}", srv.name);
let _ = archive_server(srv, pool);
println!("{} archived", srv.name);
}
}
async fn check_still_online(docker: &Docker, pool: &DbPool, srv: &Servers) {
use schema::servers::dsl::*;
if srv.status != ServerStatus::Running { return ; }
match docker.inspect_container(&format!("minecraft-{}", srv.name), None).await {
Ok(_) => {},
Err(_) => {
println!("Failed to find minecraft-{} container, setting it as Stopped", srv.name);
let conn = &mut pool.get().unwrap();
let _ = diesel::update(servers).filter(name.eq(&srv.name)).set(status.eq(ServerStatus::Stopped)).execute(conn);
},
}
}

View File

@ -5,3 +5,6 @@ mod client;
mod socket; mod socket;
pub use socket::mc_socket_listen; pub use socket::mc_socket_listen;
mod manager_loop;
pub use manager_loop::server_manager_loop;

View File

@ -1,4 +1,5 @@
use crate::minecraft::client::client::Client; use crate::minecraft::{client::client::Client, server_manager_loop};
use bollard::Docker;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::DbPool; use crate::DbPool;
@ -7,13 +8,26 @@ use tokio::net::TcpListener;
use std::io; use std::io;
pub async fn mc_socket_listen(pool: DbPool) -> io::Result<()> {
pub async fn mc_socket_listen(pool: DbPool, docker: Docker) -> io::Result<()> {
let mc_listener = TcpListener::bind("0.0.0.0:25565").await?; let mc_listener = TcpListener::bind("0.0.0.0:25565").await?;
{
let cloned_pool = pool.clone();
let cloned_docker = docker.clone();
tokio::spawn(async move {
server_manager_loop(cloned_pool, cloned_docker).await;
});
}
loop { loop {
if let Ok((socket, _)) = mc_listener.accept().await { if let Ok((socket, _)) = mc_listener.accept().await {
let cloned = pool.clone(); let cloned_pool = pool.clone();
let cloned_docker = docker.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = process_mc_socket(socket, cloned).await { if let Err(e) = process_mc_socket(socket, cloned_pool, cloned_docker).await {
eprintln!("mc error: {:?}", e); eprintln!("mc error: {:?}", e);
} }
}); });
@ -21,10 +35,10 @@ pub async fn mc_socket_listen(pool: DbPool) -> io::Result<()> {
} }
} }
async fn process_mc_socket(stream: TcpStream, pool: DbPool) -> Result<(), String> { async fn process_mc_socket(stream: TcpStream, pool: DbPool, docker: Docker) -> Result<(), String> {
println!("new client {:?}", stream); println!("new client {:?}", stream);
let mut client = Client::create(stream, pool); let mut client = Client::create(stream, pool, docker);
loop { loop {
tokio::select! { tokio::select! {

View File

@ -1,34 +1,29 @@
use std::fmt; use std::fmt;
use std::time::SystemTime;
use chrono::{DateTime, Utc}; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use serde::Deserialize;
use crate::status::ServerStatus; use crate::status::ServerStatus;
use crate::schema; use crate::schema;
#[derive(Queryable, Selectable, Debug)] #[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = schema::servers)] #[diesel(table_name = schema::servers)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Servers { pub struct Servers {
pub id: i64,
pub name: String, pub name: String,
pub last_login: Option<SystemTime>, pub last_login: Option<NaiveDateTime>,
pub container_id: Option<String>,
pub status: ServerStatus, pub status: ServerStatus,
pub redirect_ip: Option<String> pub redirect_ip: Option<String>,
pub simple_redirect: bool,
} }
#[derive(Deserialize, Insertable)] #[derive(Insertable)]
#[diesel(table_name = schema::servers)] #[diesel(table_name = schema::servers)]
pub struct CreateServer<'a> { pub struct CreateServer<'a> {
pub name: &'a str, pub name: &'a str,
pub last_login: Option<SystemTime>, pub last_login: Option<NaiveDateTime>,
pub container_id: Option<&'a str>,
pub status: ServerStatus, pub status: ServerStatus,
pub redirect_ip: Option<&'a str> pub redirect_ip: Option<&'a str>
} }
@ -38,7 +33,7 @@ impl fmt::Display for Servers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let login = match self.last_login { let login = match self.last_login {
Some(date) => { Some(date) => {
let datetime: DateTime<Utc> = date.into(); let datetime: DateTime<Utc> = Utc.from_utc_datetime(&date);
format!("{}", datetime.format("%d-%m-%Y at %H:%M").to_string()) format!("{}", datetime.format("%d-%m-%Y at %H:%M").to_string())
}, },

View File

@ -1,16 +1,12 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
diesel::table! { diesel::table! {
servers (id) { servers (name) {
id -> Int8, name -> Text,
#[max_length = 255]
name -> Varchar,
last_login -> Nullable<Timestamp>, last_login -> Nullable<Timestamp>,
#[max_length = 30] status -> SmallInt,
container_id -> Nullable<Varchar>,
status -> Int2,
is_default -> Bool, is_default -> Bool,
#[max_length = 50] redirect_ip -> Nullable<Text>,
redirect_ip -> Nullable<Varchar>, simple_redirect -> Bool,
} }
} }

36
src/srvmgr/archive.rs Normal file
View File

@ -0,0 +1,36 @@
use crate::{ models::Servers, DbPool, schema, status::ServerStatus};
use diesel::prelude::*;
pub async fn archive_server(server: &Servers, pool: &DbPool) -> Result<(), String> {
use schema::servers::dsl::*;
let conn = &mut pool.get().unwrap();
let config = crate::Config::load();
diesel::update(servers).filter(name.eq(&server.name)).set(status.eq(ServerStatus::Archiving)).execute(conn).map_err(|e| format!("Failed to set the server as archiving :{}", e))?;
println!("Starting compression (this might take a while)");
let cmd = format!("cd {} && tar cf /dev/stdout {} | lrzip - -Q -o {}.tar.lrz", config.config_path, server.name, server.name);
let _ = async_process::Command::new("sh").arg("-c").arg(cmd).output().await.map_err(|e| format!("compression failed : {}", e))?;
std::fs::remove_dir_all(config.config_path.to_owned() + "/" + &server.name).map_err(|e| format!("Failed to delete server directory: {}", e))?;
diesel::update(servers).filter(name.eq(&server.name)).set(status.eq(ServerStatus::Archived)).execute(conn).map_err(|e| format!("Failed to set the server as achived :{}", e))?;
Ok(())
}
pub async fn unarchive_server(server: &Servers) -> Result<(), String> {
let config = crate::Config::load();
println!("Starting decompression (this might take a while)");
let cmd = format!("cd {} && cat {}.tar.lrz | lrzip -d - | tar x", config.config_path, server.name);
let _ = async_process::Command::new("sh").arg("-c").arg(cmd).output().await.map_err(|e| format!("decompression failed : {}", e))?;
std::fs::remove_file(config.config_path + "/" + &server.name + ".tar.lrz").map_err(|e| format!("Failed to delete the archive of the server : {}", e))?;
Ok(())
}

10
src/srvmgr/mod.rs Normal file
View File

@ -0,0 +1,10 @@
mod start;
pub use start::start_server;
pub mod utils;
mod stop;
pub use stop::stop_server;
mod archive;
pub use archive::archive_server;

93
src/srvmgr/start.rs Normal file
View File

@ -0,0 +1,93 @@
use bollard::{Docker, plugin::{ContainerCreateBody, HostConfig, Mount, MountType}, query_parameters::CreateContainerOptionsBuilder};
use diesel::{prelude::*, result::Error::NotFound};
use crate::{models, schema, srvmgr::{archive::unarchive_server, utils::get_image_tag}, status::ServerStatus};
use crate::{DbPool, srvmgr::utils::{self, get_container_ip}};
async fn start_docker_container(docker: &Docker, name: &String) -> Result<String, String> {
let container_name = format!("minecraft-{}", name);
if let Some(_) = utils::get_container(&docker, &container_name).await {
return Err("Container already exists".to_string())
}
let config_path = crate::Config::load().config_path;
let env_file = std::fs::read_to_string(config_path.clone() + "/" + name + "/env").map_err(|e| format!("Failed to read env file {}", e))?;
let env = env_file.split("\n").filter_map(|x| if x.len() == 0 { None } else { Some(x.to_string()) } ).collect();
let image_tag = match get_image_tag(&env).await {
Some(tag) => {tag},
None => { return Err("env missing IMAGE_TAG".to_string()); }
};
utils::check_image(&docker, &image_tag).await?;
let mut mount = Mount::default();
mount.typ = Some(MountType::BIND);
mount.target = Some("/data".to_string());
mount.source = Some(config_path + "/" + name + "/server");
mount.read_only = Some(false);
let mut host_config = HostConfig::default();
host_config.mounts = Some(vec![mount]);
host_config.auto_remove = Some(true);
let config = crate::Config::load();
let mut create_config = ContainerCreateBody::default();
create_config.tty = Some(true);
create_config.open_stdin = Some(true);
create_config.user = Some("1000:100".to_string());
create_config.stop_timeout = Some(60);
create_config.host_config = Some(host_config);
create_config.image = Some(config.docker_image + ":" + &image_tag);
create_config.env = Some(env);
let options = CreateContainerOptionsBuilder::default()
.name(&container_name)
.build();
docker.create_container(Some(options), create_config).await.map_err(|e| format!("failed to create the container : {}", e))?;
println!("created container");
println!("starting container");
docker.start_container(&container_name, None).await.map_err(|e| format!("Failed to start the container : {}", e))?;
Ok(get_container_ip(docker, &container_name).await?)
}
pub async fn start_server(docker: &Docker, pool: &DbPool, server_name: &String) -> Result<(), String> {
use schema::servers::dsl::*;
let conn = &mut pool.get().unwrap();
let server = match servers.select(models::Servers::as_select()).filter(name.eq(server_name)).first(conn) {
Err(NotFound) => { return Err("This server does not exist".to_string()); },
Err(e) => { return Err(format!("Failed to get server from db : {}", e)) },
Ok(server) => { server }
};
if server.status == ServerStatus::Starting {
return Err("Server is starting, please wait".to_string());
}
if server.status != ServerStatus::Stopped && server.status != ServerStatus::Archived {
return Err(format!("Invalid server state ({}), refusing to start", server.status));
}
diesel::update(servers).filter(name.eq(server_name)).set(status.eq(ServerStatus::Starting)).execute(conn).map_err(|e| format!("Failed to set server to starting : {}", e))?;
if server.status == ServerStatus::Archived {
unarchive_server(&server).await?;
}
let ip = start_docker_container(docker, server_name).await?;
diesel::update(servers).filter(name.eq(server_name)).set((status.eq(ServerStatus::Running), redirect_ip.eq(ip.to_string() + ":25565"))).execute(conn).map_err(|e| format!("Failed to set server to starting : {}", e))?;
Ok(())
}

41
src/srvmgr/stop.rs Normal file
View File

@ -0,0 +1,41 @@
use bollard::{Docker, query_parameters::StopContainerOptionsBuilder};
use diesel::result::Error::NotFound;
use diesel::prelude::*;
use crate::{ DbPool, models, schema, status::ServerStatus };
async fn stop_container(docker: &Docker, name: &String) -> Result<(), String> {
let container_name = format!("minecraft-{}", name);
let options = StopContainerOptionsBuilder::new()
.t(60)
.build();
docker.stop_container(&container_name, Some(options)).await.map_err(|e| format!("Failed to stop the container : {}", e))?;
Ok(())
}
pub async fn stop_server(docker: &Docker, pool: &DbPool, server_name: &String) -> Result<(), String> {
use schema::servers::dsl::*;
let conn = &mut pool.get().unwrap();
let server = match servers.select(models::Servers::as_select()).filter(name.eq(server_name)).first(conn) {
Err(NotFound) => { return Err("This server does not exist".to_string()); },
Err(e) => { return Err(format!("Failed to get server from db : {}", e)) },
Ok(server) => { server }
};
if server.status != ServerStatus::Running {
return Err(format!("Invalid server state ({}), refusing to stop the server", server.status));
}
diesel::update(servers).filter(name.eq(server_name)).set(status.eq(ServerStatus::Stopping)).execute(conn).map_err(|e| format!("Failed to set the server state to stopping : {}", e))?;
stop_container(docker, server_name).await?;
diesel::update(servers).filter(name.eq(server_name)).set((redirect_ip.eq(None::<String>), status.eq(ServerStatus::Stopped))).execute(conn).map_err(|e| format!("Failed to set the server state to stopped : {}", e))?;
Ok(())
}

101
src/srvmgr/utils.rs Normal file
View File

@ -0,0 +1,101 @@
use std::collections::HashMap;
use bollard::{
Docker,
plugin::{ContainerInspectResponse},
query_parameters::{CreateImageOptionsBuilder, ListImagesOptionsBuilder}
};
use futures_util::stream::StreamExt;
pub async fn check_image(docker: &Docker, tag: &String) -> Result<(), String> {
let mut filters = HashMap::new();
let config = crate::Config::load();
let container_name = config.docker_image + ":" + tag;
filters.insert("reference", vec![&container_name]);
let options = ListImagesOptionsBuilder::default()
.all(true)
.filters(&filters)
.build();
let images = docker.list_images(Some(options)).await.map_err(|e| format!("failed to check if image exists, error : {:?}", e))?;
if images.len() >= 1 {
return Ok(());
}
let options = CreateImageOptionsBuilder::default()
.from_image(&container_name)
.tag(tag)
.build();
println!("pulling {} from docker hub ... (this might take a while and I'm too lazy to do an Interface to show the progress, if you want progress info, pull it yourself on with `docker pull {}`)", container_name, container_name);
let mut pull_result = docker.create_image(Some(options), None, None);
while let Some(update) = pull_result.next().await {
if let Err(e) = update {
return Err(format!("Failed to pull the image from docker hub, error : {:?}", e));
}
}
println!("pulled successfully");
Ok(())
}
pub async fn get_container(docker: &Docker, name: &String) -> Option<ContainerInspectResponse> {
println!("get {}", name);
let result = docker.inspect_container(name, None).await.map_err(|e| format!("failed to retreive container : {:?}", e));
match result {
Err(_) => { None },
Ok(container) => { Some(container) }
}
}
pub async fn get_container_ip(docker: &Docker, name: &String) -> Result<String, String>{
let mut retries = 20;
while retries > 0 {
let container = get_container(docker, name).await;
if let Some(container) = container && let Some(network) = container.network_settings.unwrap().networks.unwrap().get("bridge") {
if let Some(ip) = &network.ip_address {
return Ok(ip.to_string());
}
}
async_std::task::sleep(std::time::Duration::from_millis(500)).await;
retries -= 1;
}
Ok("can't get container ip".to_string())
}
pub async fn get_image_tag(env: &Vec<String>) -> Option<String> {
for var in env {
match var.split_once("=") {
Some(split) => { if split.0 == "IMAGE_TAG" { return Some(split.1.to_string()); }},
None => { },
}
}
None
}

View File

@ -1,61 +1,54 @@
use diesel::deserialize::{self, FromSql, FromSqlRow};
use diesel::pg::PgValue;
use diesel::expression::AsExpression;
use diesel::pg::Pg;
use diesel::serialize::{self, ToSql, Output};
use diesel::sql_types::SmallInt;
use std::fmt; use std::fmt;
use std::io::Write; use diesel::{deserialize::{FromSql, FromSqlRow}, expression::AsExpression, serialize::{Output, ToSql}, sql_types::SmallInt, sqlite::{Sqlite, SqliteValue}};
#[derive(Debug, Clone, Copy, PartialEq, Eq, AsExpression, FromSqlRow, serde::Serialize, serde::Deserialize)] use num_traits::FromPrimitive;
use num_derive::FromPrimitive;
use serde::{Deserialize, Serialize};
#[derive(FromSqlRow, Debug, AsExpression, FromPrimitive, Serialize, Deserialize, PartialEq, Clone)]
#[diesel(sql_type = SmallInt)] #[diesel(sql_type = SmallInt)]
#[repr(i16)]
pub enum ServerStatus { pub enum ServerStatus {
Archived = 0, Archived = 0,
Stopped = 1, Stopped = 1,
Stopping = 2, Stopping = 2,
Starting = 3, Starting = 3,
Running = 4, Running = 4,
Unknown = 5 Archiving = 5,
Unknown,
} }
impl ToSql<SmallInt, Pg> for ServerStatus { impl FromSql<SmallInt, Sqlite> for ServerStatus{
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
let val = *self as i16; let t = <i16 as FromSql<SmallInt, Sqlite>>::from_sql(bytes)?;
out.write_all(&val.to_be_bytes())?; if let Some(status) = ServerStatus::from_i16(t) {
Ok(serialize::IsNull::No) return Ok(status)
}
Ok(ServerStatus::Unknown)
} }
} }
impl FromSql<SmallInt, Pg> for ServerStatus { impl ToSql<SmallInt, Sqlite> for ServerStatus {
fn from_sql(value: PgValue<'_>) -> deserialize::Result<Self> { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result {
let bytes = value.as_bytes(); let value: i16 = self.clone() as i16;
if bytes.len() != 2 { out.set_value(value as i32);
return Err("Invalid length for i16".into()); Ok(diesel::serialize::IsNull::No)
}
let val = i16::from_be_bytes([bytes[0], bytes[1]]);
match val {
0 => Ok(ServerStatus::Archived),
1 => Ok(ServerStatus::Stopped),
2 => Ok(ServerStatus::Stopping),
3 => Ok(ServerStatus::Starting),
4 => Ok(ServerStatus::Running),
_ => Ok(ServerStatus::Unknown),
}
} }
} }
impl fmt::Display for ServerStatus { impl fmt::Display for ServerStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str_status = match self { let str_status = match self {
ServerStatus::Archived => "archieved", ServerStatus::Archived => "archived",
ServerStatus::Archiving => "archiving",
ServerStatus::Stopped => "stopped", ServerStatus::Stopped => "stopped",
ServerStatus::Stopping => "stopping", ServerStatus::Stopping => "stopping",
ServerStatus::Starting => "starting", ServerStatus::Starting => "starting",
ServerStatus::Running => "running", ServerStatus::Running => "running",
ServerStatus::Unknown => "(error : unknown status)" ServerStatus::Unknown => "(error : unknown status)",
}; };
write!(f, "{}", str_status) write!(f, "{}", str_status)
} }