Compare commits

...

5 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
19 changed files with 184 additions and 60 deletions

37
Cargo.lock generated
View File

@ -174,6 +174,42 @@ dependencies = [
"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]]
name = "async-std"
version = "1.13.2"
@ -609,6 +645,7 @@ dependencies = [
name = "dockermcmgr"
version = "0.0.6"
dependencies = [
"async-process",
"async-std",
"bollard",
"chrono",

View File

@ -1,6 +1,6 @@
[package]
name = "dockermcmgr"
version = "0.0.6"
version = "0.0.7"
edition = "2024"
[[bin]]
@ -8,6 +8,7 @@ name = "dmm"
path = "src/main.rs"
[dependencies]
async-process = "2.5.0"
async-std = "1.13.2"
bollard = "0.21.0"
chrono = "0.4.44"
@ -22,5 +23,5 @@ num-traits = "0.2.19"
regex = "1.12.3"
serde = "1.0.228"
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"

View File

@ -38,7 +38,7 @@
final:
{
pname = "dmm";
version = "0.0.6";
version = "0.0.7";
inherit src;
cargoLock.lockFile = src + "/Cargo.lock";
@ -46,10 +46,13 @@
doCheck = false;
buildInputs =
with pkgs; [ sqlite ];
# env.COMPRESSION_COMMAND="${pkgs.lrzip}/bin/lrzip";
buildInputs = with pkgs; [
sqlite
lrzip
];
}
// specialArgs
);
in

View File

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

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

@ -5,6 +5,7 @@ pub mod ls;
pub mod start;
pub mod stop;
pub mod set_state;
pub mod archive;
pub mod env_settings;

View File

@ -1,7 +1,7 @@
use bollard::Docker;
use clap::{Arg, ArgMatches, Command};
use crate::{Config, DbPool, dockermgr};
use crate::{Config, DbPool, srvmgr};
use std::fs;
@ -21,7 +21,7 @@ pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: &Docker)
let conn = &mut pool.get().unwrap();
let _ = dockermgr::stop_server(docker, pool, server_name).await;
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())?;
if deleted != 1 {

View File

@ -5,7 +5,7 @@ use clap::{Command, ArgMatches};
use crate::{ DbPool, schema };
use crate::dockermgr;
use crate::srvmgr;
use diesel::prelude::*;
@ -18,7 +18,7 @@ pub fn create_subcommand() -> Command {
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();
dockermgr::start_server(&docker, pool, server_name).await?;
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))?;

View File

@ -4,7 +4,7 @@ use clap::{Command, ArgMatches};
use crate::DbPool;
use crate::dockermgr;
use crate::srvmgr;
pub fn create_subcommand() -> Command {
Command::new("stop")
@ -15,7 +15,7 @@ pub fn create_subcommand() -> Command {
pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: &Docker) -> Result<(), String> {
let server_name = arguments.get_one::<String>("NAME").unwrap();
println!("Stopping container");
dockermgr::stop_server(&docker, pool, server_name).await?;
srvmgr::stop_server(&docker, pool, server_name).await?;
println!("container stopped");
Ok(())

View File

@ -6,7 +6,7 @@
/* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/05/29 21:22:17 by tomoron #+# #+# */
/* Updated: 2026/06/11 23:41:49 by tomoron ### ########.fr */
/* Updated: 2026/06/12 18:32:19 by tomoron ### ########.fr */
/* */
/* ************************************************************************** */
@ -20,7 +20,7 @@ pub mod models;
pub mod status;
pub mod dockermgr;
pub mod srvmgr;
mod cli;
use cli::*;
@ -63,9 +63,10 @@ fn cli() -> Command {
.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> {
let arg = cli().get_matches();
let pool = get_connection_pool();
@ -80,10 +81,11 @@ async fn main() -> Result<(), String> {
Some(("remove", sub_matches)) => { remove::subcommand(sub_matches, &pool, &docker).await?; },
Some(("ls", sub_matches)) => { ls::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(("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")}
}
Ok(())

View File

@ -8,10 +8,6 @@ pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
pub fn run_migrations(pool: &DbPool) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
// This will run the necessary migrations.
//
// See the documentation for `MigrationHarness` for
// all available methods.
let connection = &mut pool.get().unwrap();
connection.run_pending_migrations(MIGRATIONS)?;

View File

@ -1,13 +1,10 @@
use crate::minecraft::client::client::Client;
use crate::{srvmgr::start_server, minecraft::client::client::Client};
use diesel::result::Error::NotFound;
use json::object;
use crate::minecraft::varint::varint_write;
use std::collections::VecDeque;
use crate::{ dockermgr };
impl Client {
pub async fn login_intent_handle(&mut self, mut _packet: &mut VecDeque<u8>, _packet_id: i32) -> Result<(),String> {
@ -30,7 +27,8 @@ impl Client {
Err(e) => { return Err(format!("db fetch error : {}", e)); }
};
dockermgr::start_server(&self.docker, &self.db_pool, &server.name).await?;
start_server(&self.docker, &self.db_pool, &server.name).await?;
Ok(())
}

View File

@ -1,45 +1,27 @@
use bollard::{Docker, query_parameters::{ListContainersOptionsBuilder, StopContainerOptionsBuilder}};
use bollard::Docker;
use chrono::{TimeDelta, Utc};
use crate::{ DbPool, dockermgr, models::{self, Servers}, schema, status::ServerStatus };
use crate::{ DbPool, models::{self, Servers}, schema, srvmgr::{self, archive_server}, status::ServerStatus };
use async_std::task;
use std::{collections::HashMap, time::Duration};
use std::time::Duration;
use diesel::prelude::*;
async fn server_init(pool: &DbPool, docker: &Docker) -> Result<(), String> {
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)).set(status.eq(ServerStatus::Stopped)).execute(conn);
let mut filters = HashMap::new();
filters.insert("name", vec!["minecraft-"]);
let container_list_options = ListContainersOptionsBuilder::new()
.all(true)
.filters(&filters)
.build();
let containers = docker.list_containers(Some(container_list_options)).await.map_err(|e| format!("Failed get current containers : {}", e))?;
if containers.len() != 0 {
println!("stopping {} stray container(s)", containers.len())
}
for container in containers {
let container_name = &container.names.unwrap()[0][1..];
println!("stopping {}", container_name);
let stop_options = StopContainerOptionsBuilder::new()
.t(60)
.build();
docker.stop_container(container_name, Some(stop_options)).await.map_err(|e| format!("Failed to stop container {}, : {}", container_name, e))?;
println!("{} stopped", container_name);
}
println!("init done");
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(())
}
@ -63,6 +45,7 @@ pub async fn server_manager_loop(pool: DbPool, docker: Docker)
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;
@ -74,11 +57,21 @@ async fn should_stop(docker: &Docker, pool: &DbPool, diff: &TimeDelta, srv: &Ser
if diff.num_days() >= 7 {
println!("last login time exceeded, stopping {}", srv.name);
let _ = dockermgr::stop_server(docker, pool, &srv.name).await;
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::*;

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(())
}

View File

@ -5,3 +5,6 @@ pub mod utils;
mod stop;
pub use stop::stop_server;
mod archive;
pub use archive::archive_server;

View File

@ -1,9 +1,9 @@
use bollard::{Docker, plugin::{ContainerCreateBody, HostConfig, Mount, MountType}, query_parameters::CreateContainerOptionsBuilder};
use diesel::{prelude::*, result::Error::NotFound};
use crate::{dockermgr::utils::get_image_tag, models, schema, status::ServerStatus};
use crate::{models, schema, srvmgr::{archive::unarchive_server, utils::get_image_tag}, status::ServerStatus};
use crate::{DbPool, dockermgr::utils::{self, get_container_ip}};
use crate::{DbPool, srvmgr::utils::{self, get_container_ip}};
async fn start_docker_container(docker: &Docker, name: &String) -> Result<String, String> {
@ -75,12 +75,17 @@ pub async fn start_server(docker: &Docker, pool: &DbPool, server_name: &String)
return Err("Server is starting, please wait".to_string());
}
if server.status != ServerStatus::Stopped {
return Err(format!("Invalid server state({}), refusing to start", server.status));
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))?;

View File

@ -16,7 +16,8 @@ pub enum ServerStatus {
Stopping = 2,
Starting = 3,
Running = 4,
Unknown = 5,
Archiving = 5,
Unknown,
}
impl FromSql<SmallInt, Sqlite> for ServerStatus{
@ -42,6 +43,7 @@ impl fmt::Display for ServerStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str_status = match self {
ServerStatus::Archived => "archived",
ServerStatus::Archiving => "archiving",
ServerStatus::Stopped => "stopped",
ServerStatus::Stopping => "stopping",
ServerStatus::Starting => "starting",