Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
b693f0cda9 | |||
16f2f26236 | |||
dc9b3a7262 | |||
da099a11ce | |||
1723c26ba9 |
23
.gitignore
vendored
23
.gitignore
vendored
@ -1,2 +1,21 @@
|
||||
# NPM dependencies
|
||||
node_modules/
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
30
.woodpecker.yml
Normal file
30
.woodpecker.yml
Normal file
@ -0,0 +1,30 @@
|
||||
pipeline:
|
||||
tests:
|
||||
image: rust:1.63
|
||||
commands:
|
||||
- "cargo test"
|
||||
|
||||
build_release:
|
||||
image: rust:1.63
|
||||
commands:
|
||||
- "cargo install cargo-deb cargo-generate-rpm"
|
||||
- "cargo build --release"
|
||||
- "cargo deb"
|
||||
- "cargo generate-rpm"
|
||||
- "mv target/release/mcst target/release/mcst-${CI_COMMIT_TAG}-linux-x86_64"
|
||||
when:
|
||||
event: tag
|
||||
|
||||
gitea_release:
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_api_key
|
||||
base_url: https://git.metaunix.net
|
||||
files:
|
||||
- "target/release/*${CI_COMMIT_TAG}-linux-x86_64"
|
||||
- "target/debian/mcst*.deb"
|
||||
- "target/generate-rpm/mcst*.rpm"
|
||||
title: "${CI_COMMIT_TAG}"
|
||||
when:
|
||||
event: tag
|
30
Cargo.toml
Normal file
30
Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "mcst"
|
||||
description = "Bit Goblin Minecraft server management tool."
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
license = "BSD 2-Clause"
|
||||
authors = ["Gregory Ballantine <gballantine@bitgoblin.tech>"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "3.2", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
shellexpand = "2.1"
|
||||
|
||||
[package.metadata.deb]
|
||||
license-file = "LICENSE"
|
||||
depends = "openjdk-17-jre"
|
||||
section = "games"
|
||||
assets = [
|
||||
["target/release/mcst", "usr/bin/mcst", "755"],
|
||||
["README.md", "usr/share/doc/zealot/README", "644"]
|
||||
]
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
{ source = "target/release/mcst", dest = "/usr/bin/mcst", mode = "755" },
|
||||
{ source = "README.md", dest = "/usr/share/doc/mcst/README", mode = "644"}
|
||||
]
|
||||
[package.metadata.generate-rpm.requires]
|
||||
java-17-openjdk = "*"
|
@ -1,64 +0,0 @@
|
||||
const { exec } = require("child_process");
|
||||
const fs = require('fs');
|
||||
|
||||
class Server {
|
||||
constructor(dir) {
|
||||
this.rootDir = dir;
|
||||
this.name = dir.split('/').at(-1);
|
||||
this.pidFilePath = this.rootDir + '/pid.txt';
|
||||
|
||||
// read version file
|
||||
var versionFilePath = this.rootDir + '/current_version.txt';
|
||||
if (fs.existsSync(versionFilePath)) {
|
||||
this.version = fs.readFileSync(versionFilePath, {encoding:'utf8', flag:'r'});
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log(`Starting server ${this.name}...`);
|
||||
|
||||
// create shell command and execute it
|
||||
let cmd = `${this.rootDir}/start.sh`;
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.log(`Error while starting server: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(`Server ${this.name} has been started.`);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
console.log(`Stopping server ${this.name}...`);
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
if (fs.existsSync(this.pidFilePath)) {
|
||||
let pid = fs.readFileSync(this.pidFilePath, {encoding: 'utf8', flag: 'r'});
|
||||
try {
|
||||
return process.kill(pid, 0);
|
||||
} catch (err) {
|
||||
// ESRCH error means the process doesn't exist, so should return false
|
||||
if (err.code !== 'ESRCH') {
|
||||
console.error(err);
|
||||
return err.code === 'EPERM';
|
||||
} else {
|
||||
// delete pid.txt so we don't have to worry about checking it again
|
||||
fs.unlinkSync(this.pidFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
getPid() {
|
||||
if (fs.existsSync(this.pidFilePath)) {
|
||||
return fs.readFileSync(this.pidFilePath, {encoding:'utf8', flag:'r'})
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Server = Server;
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"server_directory": "/opt/minecraft"
|
||||
}
|
30
index.js
30
index.js
@ -1,30 +0,0 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// set template engine to Pug
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
// Using express.urlencoded middleware
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
|
||||
// import route handlers
|
||||
var homeRoutes = require('./routes/home');
|
||||
var serverRoutes = require('./routes/server');
|
||||
|
||||
// define routes
|
||||
app.get('/', homeRoutes.getIndex);
|
||||
app.get('/server/create', serverRoutes.getCreate);
|
||||
app.post('/server/create', serverRoutes.postCreate);
|
||||
app.get('/server/:serverName/start', serverRoutes.getStart);
|
||||
app.get('/server/:serverName/stop', serverRoutes.getStop);
|
||||
|
||||
// set Express to serve static files (ideally this is only used in development)
|
||||
app.use(express.static('./static/'));
|
||||
|
||||
// start Express.js app
|
||||
app.listen(port, () => {
|
||||
console.log(`MCST has started and is listening on port ${port}.`);
|
||||
});
|
1706
package-lock.json
generated
1706
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "mcst",
|
||||
"version": "0.1.0",
|
||||
"description": "Minecraft Java edition server management tool",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "gitea@git.metaunix.net:BitGoblin/mcst.git"
|
||||
},
|
||||
"keywords": [
|
||||
"minecraft",
|
||||
"minecraft java edition",
|
||||
"java"
|
||||
],
|
||||
"author": "Gregory Ballantine <gballantine@bitgoblin.tech>",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"config": "^3.3.8",
|
||||
"express": "^4.18.1",
|
||||
"pug": "^3.0.2"
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
const config = require('config');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const minecraft = require('../app/MinecraftServer');
|
||||
|
||||
exports.getIndex = function(req, res) {
|
||||
// search for minecraft server directories
|
||||
let rootDir = config.get('server_directory');
|
||||
let serverDirs = fs.readdirSync(rootDir);
|
||||
let servers = [];
|
||||
for (let i = 0; i < serverDirs.length; i++) {
|
||||
servers.push(new minecraft.Server(path.join(rootDir, serverDirs[i])));
|
||||
}
|
||||
|
||||
// render view
|
||||
res.render('index', {
|
||||
servers: servers,
|
||||
});
|
||||
};
|
@ -1,70 +0,0 @@
|
||||
const config = require('config');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const minecraft = require('../app/MinecraftServer');
|
||||
|
||||
exports.getCreate = function(req, res) {
|
||||
// render view
|
||||
res.render('create');
|
||||
};
|
||||
|
||||
exports.postCreate = function(req, res) {
|
||||
mcDir = config.get('server_directory');
|
||||
serverDir = path.join(mcDir, req.body.serverName);
|
||||
|
||||
// make server directory
|
||||
fs.mkdirSync(serverDir);
|
||||
|
||||
// grab the server JAR file
|
||||
serverJarUrl = 'https://piston-data.mojang.com/v1/objects/f69c284232d7c7580bd89a5a4931c3581eae1378/server.jar';
|
||||
serverJarName = "server_" + req.body.serverVersion + ".jar";
|
||||
serverJarPath = path.join(serverDir, serverJarName);
|
||||
https.get(serverJarUrl, (res) => {
|
||||
const filePath = fs.createWriteStream(serverJarPath);
|
||||
res.pipe(filePath);
|
||||
filePath.on('finish', () => {
|
||||
filePath.close();
|
||||
console.log('Download Completed');
|
||||
});
|
||||
});
|
||||
|
||||
// create the start.sh shell script
|
||||
scriptFilePath = path.join(serverDir, 'start.sh');
|
||||
scriptContent = "#!/bin/sh\n\ncd " + serverDir + "\njava -Xmx2048M -Xms2048M -jar server_" + req.body.serverVersion + ".jar nogui &\n\necho \"$!\" > ./pid.txt";
|
||||
fs.writeFileSync(scriptFilePath, scriptContent);
|
||||
fs.chmodSync(scriptFilePath, 0o755);
|
||||
|
||||
// save the current version to a text file - this will hopefully be temporary solution
|
||||
versionFilePath = path.join(serverDir, 'current_version.txt');
|
||||
versionContent = req.body.serverVersion;
|
||||
fs.writeFileSync(versionFilePath, versionContent);
|
||||
fs.chmodSync(versionFilePath, 0o644);
|
||||
|
||||
// redirect the user back to the home page
|
||||
res.redirect('/');
|
||||
};
|
||||
|
||||
exports.getStart = function(req, res) {
|
||||
let serverName = req.params.serverName;
|
||||
let rootDir = config.get('server_directory');
|
||||
let server = new minecraft.Server(path.join(rootDir, serverName));
|
||||
|
||||
// start the server
|
||||
server.start();
|
||||
|
||||
// redirect the user back to the home page
|
||||
res.redirect('/');
|
||||
};
|
||||
|
||||
exports.getStop = function(req, res) {
|
||||
let serverName = req.params.serverName;
|
||||
let rootDir = config.get('server_directory');
|
||||
let server = new minecraft.Server(path.join(rootDir, serverName));
|
||||
|
||||
// stop the server
|
||||
server.stop();
|
||||
|
||||
// redirect the user back to the home page
|
||||
res.redirect('/');
|
||||
};
|
2
src/cmd/mod.rs
Normal file
2
src/cmd/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod new;
|
||||
pub mod start;
|
37
src/cmd/new.rs
Normal file
37
src/cmd/new.rs
Normal file
@ -0,0 +1,37 @@
|
||||
extern crate reqwest;
|
||||
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
|
||||
pub fn new_command(server_name: &str, minecraft_version: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Creating new server with name '{}' using version '{}'.", server_name, minecraft_version);
|
||||
|
||||
let home_path = shellexpand::tilde("~");
|
||||
let server_directory_path = format!("{}/{}", home_path, server_name);
|
||||
let server_file_path = format!("{}/server_{}.jar", server_directory_path, minecraft_version);
|
||||
|
||||
// create the server directory
|
||||
println!("Creating server directory {}.", server_directory_path);
|
||||
fs::create_dir(&server_directory_path)?;
|
||||
|
||||
// download the Minecraft server JAR file
|
||||
let server_jar_url = "https://piston-data.mojang.com/v1/objects/f69c284232d7c7580bd89a5a4931c3581eae1378/server.jar";
|
||||
println!("Downloading {} to {}.", server_jar_url, server_file_path);
|
||||
let mut resp = reqwest::blocking::get(server_jar_url)?;
|
||||
let mut out = fs::File::create(server_file_path)?;
|
||||
io::copy(&mut resp, &mut out)?;
|
||||
|
||||
// create the start.sh shell script
|
||||
let script_file_path = format!("{}/start.sh", server_directory_path);
|
||||
println!("Creating start.sh script.");
|
||||
let mut script_file = fs::File::create(script_file_path)?;
|
||||
let script_file_contents = format!("#!/bin/sh\n\ncd {}\njava -Xmx2048M -Xms2048M -jar server_{}.jar nogui", server_directory_path, minecraft_version);
|
||||
script_file.write_all(script_file_contents.as_bytes())?;
|
||||
// set the file permissions on the start.sh script
|
||||
script_file.set_permissions(fs::Permissions::from_mode(0o755))?;
|
||||
|
||||
// return empty result to signify everything is okay
|
||||
Ok(())
|
||||
}
|
27
src/cmd/start.rs
Normal file
27
src/cmd/start.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn start_command(server_name: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Starting server {}.", server_name);
|
||||
|
||||
// set up our path variables
|
||||
let home_path = shellexpand::tilde("~");
|
||||
let server_directory_path = format!("{}/{}", home_path, server_name);
|
||||
let script_file_path = format!("{}/start.sh", server_directory_path);
|
||||
let eula_file_path = format!("{}/eula.txt", server_directory_path);
|
||||
|
||||
// check if eula.txt exists - if it doesn't then warn the user they'll need to accept it and possibly modify server settings
|
||||
if !std::path::Path::new(&eula_file_path).exists() {
|
||||
println!("The eula.txt does not exist - you will need to accept the EULA located at {} by changing 'false' to 'true'.", eula_file_path);
|
||||
println!("This appears to be a new server instance. Don't forget to modify server.properties!");
|
||||
}
|
||||
|
||||
// run the start command
|
||||
Command::new(script_file_path)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
// return okay signal
|
||||
Ok(())
|
||||
}
|
45
src/main.rs
Normal file
45
src/main.rs
Normal file
@ -0,0 +1,45 @@
|
||||
mod cmd;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "Minecraft server management tool", author, version, about = "Bit Goblin's Minecraft server management tool.", long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
// new server subcommand
|
||||
#[clap(name = "new", about = "Create a new Minecraft java edition server instance.")]
|
||||
New {
|
||||
#[clap(short = 'n', long, required = true, help = "[REQUIRED] The name for your new server.")]
|
||||
server_name: String,
|
||||
#[clap(short = 'm', long, required = true, help = "[REQUIRED] Minecraft Java Edition server version to use.")]
|
||||
minecraft_version: String,
|
||||
},
|
||||
|
||||
#[clap(name = "start", about = "Start a Minecraft java edition server instance.")]
|
||||
Start {
|
||||
#[clap(short = 'n', long, required = true, help = "[REQUIRED] The name of your Minecraft server instance.")]
|
||||
server_name: String,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// start the Clap CLI
|
||||
let cli = Cli::parse();
|
||||
|
||||
// map subcommands back to the main command
|
||||
let res = match &cli.command {
|
||||
Commands::New { server_name, minecraft_version } => cmd::new::new_command(&server_name, &minecraft_version),
|
||||
Commands::Start { server_name } => cmd::start::start_command(&server_name),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {},
|
||||
Err(e) => panic!("MCST ran into an error: {}", e),
|
||||
};
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
body{
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
/* set the max-width for centered content */
|
||||
.container{
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
#main-content{
|
||||
margin-top: 25px;
|
||||
padding: 12px 25px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* global navigation bar styles */
|
||||
.navbar{
|
||||
padding: 10px 18px;
|
||||
background-color: #212121;
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar ul{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navbar li{
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.navbar li:not(:first-child){
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.navbar li>a{
|
||||
color: #eee;
|
||||
text-decoration: none;
|
||||
transition: color 220ms ease-in-out;
|
||||
}
|
||||
.navbar li>a:hover{
|
||||
color: white;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
$(document).ready(function () {
|
||||
// This will be used when needed
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
extends layout.pug
|
||||
|
||||
block content
|
||||
header.row
|
||||
div.columns.twelve
|
||||
h1 Create new server
|
||||
|
||||
section.row
|
||||
div.columns.twelve
|
||||
form(action='/server/create', method='POST')
|
||||
div.row
|
||||
div.columns.six
|
||||
label(for='serverName') Server name:
|
||||
input#serverName.u-full-width(name='serverName', type='text', placeholder='MyServer')
|
||||
div.columns.six
|
||||
label(for='serverVersion') Minecraft version:
|
||||
input#serverVersion.u-full-width(name='serverVersion', type='text', placeholder='1.19.2')
|
||||
|
||||
input(type='submit', value='Submit')
|
||||
|
||||
div.row
|
||||
div.columns.twelve
|
||||
p
|
||||
a(href='/') Back
|
@ -1,30 +0,0 @@
|
||||
extends layout.pug
|
||||
|
||||
block content
|
||||
header.row
|
||||
div.columns.twelve
|
||||
h1 Welcome to MCST!
|
||||
p Using MCST you can easily manage your Minecraft: Java Edition servers.
|
||||
|
||||
section.row
|
||||
div.columns.twelve
|
||||
h3 List of servers:
|
||||
if servers.length < 1
|
||||
p There are currently no servers registered.
|
||||
else
|
||||
table.u-full-width
|
||||
thead
|
||||
tr
|
||||
th Server name
|
||||
th Minecraft version
|
||||
th State
|
||||
th Actions
|
||||
tbody
|
||||
each m in servers
|
||||
tr.serverItem
|
||||
td.serverName= m.name
|
||||
td.serverVersion= m.version
|
||||
td.serverState= m.getStatus()
|
||||
td
|
||||
a(href='/server/' + m.name + '/start') Start
|
||||
a(href='/server/' + m.name + '/stop') Stop
|
@ -1,21 +0,0 @@
|
||||
doctype html
|
||||
html(lang="en")
|
||||
head
|
||||
title= pageTitle
|
||||
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css')
|
||||
link(rel='stylesheet', href='/css/wyrm.css')
|
||||
script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js')
|
||||
script(src='/js/drake.js')
|
||||
body
|
||||
// global navigation
|
||||
nav.navbar
|
||||
div.navbar-left
|
||||
ul
|
||||
li.menu-text MCST
|
||||
li: a(href='/') Home
|
||||
li: a(href='/server/create') Create
|
||||
li: a(href='/status') Status
|
||||
|
||||
// main content
|
||||
div#main-content.container
|
||||
block content
|
Reference in New Issue
Block a user