Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
9ecc458ea1 | |||
069a246db0 | |||
a5d8d74332 | |||
205ad74a51 | |||
6c23f3c806 | |||
2297768b57 | |||
5c35389ae3 | |||
fdb31eeb00 | |||
bb584d5ea0 | |||
54199bf7c8 | |||
4007b99833 | |||
efde09025c | |||
5699c9cf6a | |||
b41b246483 | |||
25b68c4bc6 | |||
1fd7bb7a9a | |||
6af39995f7 | |||
8c15b10389 |
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,21 +1,5 @@
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
# Composer dependencies
|
||||
vendor/
|
||||
|
||||
# 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
|
||||
# PHP CodeSniffer cache
|
||||
.phpcs-cache
|
||||
|
@ -1,19 +1,30 @@
|
||||
pipeline:
|
||||
tests:
|
||||
image: rust:1.63
|
||||
setup:
|
||||
image: composer:2.4
|
||||
commands:
|
||||
- "cargo test"
|
||||
- composer install
|
||||
- mkdir test
|
||||
- touch test/results.txt
|
||||
|
||||
build_release:
|
||||
image: rust:1.63
|
||||
phpcs:
|
||||
group: test
|
||||
image: composer:2.4
|
||||
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"
|
||||
- composer run-script phpcs >> test/results.txt
|
||||
phpmd:
|
||||
group: test
|
||||
image: composer:2.4
|
||||
commands:
|
||||
- composer run-script phpmd >> test/results.txt
|
||||
|
||||
notify:
|
||||
image: drillster/drone-email
|
||||
host: smtp.int.metaunix.net
|
||||
skip_verify: true
|
||||
from: drone@ci-v1.int.metaunix.net
|
||||
attachment: test/results.txt
|
||||
when:
|
||||
event: tag
|
||||
status: [ failure ]
|
||||
|
||||
gitea_release:
|
||||
image: plugins/gitea-release
|
||||
@ -21,10 +32,6 @@ pipeline:
|
||||
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
30
Cargo.toml
@ -1,30 +0,0 @@
|
||||
[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 = "*"
|
4
bin/run-php.sh
Executable file
4
bin/run-php.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
# start a local instance of the app using PHP's built-in webserver
|
||||
php -S localhost:8080 -t public/ public/index.php
|
33
composer.json
Normal file
33
composer.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "bitgoblin/mcst",
|
||||
"description": "Minecraft Java Edition server management tool",
|
||||
"type": "project",
|
||||
"license": "BSD-2-Clause",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BitGoblin\\MCST\\": "src/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gregory Ballantine",
|
||||
"email": "gballantine@bitgoblin.tech"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"slim/slim": "^4.10",
|
||||
"slim/psr7": "^1.5",
|
||||
"slim/twig-view": "^3.3",
|
||||
"hassankhan/config": "^3.0",
|
||||
"php-di/php-di": "^6.4",
|
||||
"imangazaliev/didom": "^1.13"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"phpmd/phpmd": "^2.5"
|
||||
},
|
||||
"scripts": {
|
||||
"phpcs": "phpcs --standard=./phpcs.xml",
|
||||
"phpmd": "phpmd src/ text phpmd.xml"
|
||||
}
|
||||
}
|
2213
composer.lock
generated
Normal file
2213
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
conf/defaults.json
Normal file
3
conf/defaults.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"server_directory": "/opt/minecraft"
|
||||
}
|
35
phpcs.xml
Normal file
35
phpcs.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<ruleset name="PHP_CodeSniffer">
|
||||
|
||||
<description>PHPCS configuration file.</description>
|
||||
|
||||
<arg name="basepath" value="." />
|
||||
<arg name="extensions" value="php" />
|
||||
<arg name="colors" />
|
||||
<arg name="cache" value=".phpcs-cache" />
|
||||
<arg value="p" />
|
||||
<arg value="s" />
|
||||
|
||||
<!-- Check PHP files in the src/ directory -->
|
||||
<file>src/</file>
|
||||
|
||||
<!-- Our base rule: set to PSR12-->
|
||||
<rule ref="PSR12">
|
||||
<exclude name="PSR12.Classes.OpeningBraceSpace.Found" />
|
||||
<exclude name="PSR2.Classes.ClassDeclaration.OpenBraceNewLine" />
|
||||
<exclude name="PSR2.Classes.ClassDeclaration.CloseBraceAfterBody" />
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine" />
|
||||
</rule>
|
||||
|
||||
<!-- Some custom rules -->
|
||||
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie" />
|
||||
<rule ref="Generic.Classes.OpeningBraceSameLine" />
|
||||
<!-- Set indent size to 2 -->
|
||||
<rule ref="Generic.WhiteSpace.ScopeIndent">
|
||||
<properties>
|
||||
<property name="indent" value="2" />
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
19
phpmd.xml
Normal file
19
phpmd.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="PHP CMS rule set"
|
||||
xmlns="http://pmd.sf.net/ruleset/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
|
||||
http://pmd.sf.net/ruleset_xml_schema.xsd"
|
||||
xsi:noNamespaceSchemaLocation="
|
||||
http://pmd.sf.net/ruleset_xml_schema.xsd">
|
||||
<description>
|
||||
Custom rule set for Pigeon Discord notifier project.
|
||||
</description>
|
||||
|
||||
<!-- Import some rule sets -->
|
||||
<rule ref="rulesets/cleancode.xml" />
|
||||
<rule ref="rulesets/codesize.xml" />
|
||||
<rule ref="rulesets/design.xml" />
|
||||
<rule ref="rulesets/naming.xml" />
|
||||
<rule ref="rulesets/unusedcode.xml" />
|
||||
</ruleset>
|
1
pid_manual.txt
Normal file
1
pid_manual.txt
Normal file
@ -0,0 +1 @@
|
||||
628624
|
5
public/.htaccess
Normal file
5
public/.htaccess
Normal file
@ -0,0 +1,5 @@
|
||||
# rewrite rules
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^ index.php [QSA,L]
|
58
public/css/wyrm.css
Normal file
58
public/css/wyrm.css
Normal file
@ -0,0 +1,58 @@
|
||||
body{
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
input[type=submit]:disabled,
|
||||
button:disabled{
|
||||
background-color: darkgrey;
|
||||
}
|
||||
input[type=submit]:disabled:hover,
|
||||
button:disabled:hover{
|
||||
background-color: darkgrey;
|
||||
border: 1px solid #bbb;
|
||||
color: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
24
public/index.php
Normal file
24
public/index.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// if we're looking for static files in dev, return false so they can be served.
|
||||
if (PHP_SAPI == 'cli-server') {
|
||||
$url = parse_url($_SERVER['REQUEST_URI']);
|
||||
$file = __DIR__ . $url['path'];
|
||||
|
||||
// check the file types, only serve standard files
|
||||
if (preg_match('/\.(?:png|js|jpg|jpeg|gif|css)$/', $file)) {
|
||||
// does the file exist? If so, return it
|
||||
if (is_file($file))
|
||||
return false;
|
||||
|
||||
// file does not exist. return a 404
|
||||
header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
|
||||
printf('"%s" does not exist', $_SERVER['REQUEST_URI']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../src/app.php';
|
||||
|
||||
$app->run();
|
||||
|
29
public/js/drake.js
Normal file
29
public/js/drake.js
Normal file
@ -0,0 +1,29 @@
|
||||
$(document).ready(function () {
|
||||
// periodically check for server status updates
|
||||
$('.serverItem').each(function() {
|
||||
setInterval(updateServer, 5000, $(this));
|
||||
});
|
||||
|
||||
// set the serverName input field to check if the server exists
|
||||
$('input#serverName').on('change', checkServerExists);
|
||||
});
|
||||
|
||||
function updateServer(elem) {
|
||||
var serverName = elem.data('server-name');
|
||||
$.get('/server/' + serverName + '/status', function(data, state) {
|
||||
elem.children('.serverState').eq(0).text(data.state);
|
||||
});
|
||||
}
|
||||
|
||||
function checkServerExists() {
|
||||
$.get('/server/' + $(this).val() + '/status', function(data, state) {
|
||||
if (data.exists) {
|
||||
$('input#createSubmit').prop('disabled', true);
|
||||
alert('That server name is already used; please use another name!');
|
||||
} else {
|
||||
if ($('input#createSubmit').prop('disabled')) {
|
||||
$('input#createSubmit').prop('disabled', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
19
src/Controllers/Controller.php
Normal file
19
src/Controllers/Controller.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\MCST\Controllers;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class Controller {
|
||||
|
||||
protected $container;
|
||||
|
||||
public function __construct(ContainerInterface $container) {
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function get(string $name) {
|
||||
return $this->container->get($name);
|
||||
}
|
||||
|
||||
}
|
29
src/Controllers/HomeController.php
Normal file
29
src/Controllers/HomeController.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\MCST\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
use BitGoblin\MCST\Minecraft\Server;
|
||||
|
||||
class HomeController extends Controller {
|
||||
|
||||
public function getIndex(Request $request, Response $response): Response {
|
||||
$config = $this->get('config');
|
||||
|
||||
// find servers
|
||||
$minecraftDir = $config->get('server_directory');
|
||||
$serverDirs = glob($minecraftDir . "/*", GLOB_ONLYDIR);
|
||||
$minecraftServers = array();
|
||||
foreach ($serverDirs as $m) {
|
||||
array_push($minecraftServers, new Server($m));
|
||||
}
|
||||
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'index.twig', [
|
||||
'servers' => $minecraftServers,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
151
src/Controllers/ServerController.php
Normal file
151
src/Controllers/ServerController.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\MCST\Controllers;
|
||||
|
||||
use DiDom\Document;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
use BitGoblin\MCST\Minecraft\Server;
|
||||
|
||||
class ServerController extends Controller {
|
||||
|
||||
// GET create server route
|
||||
public function getCreate(Request $request, Response $response): Response {
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'create.twig');
|
||||
}
|
||||
|
||||
// POST create server route
|
||||
public function postCreate(Request $request, Response $response): Response {
|
||||
// set up the POST parameters and grab our config
|
||||
$params = (array)$request->getParsedBody();
|
||||
$config = $this->get('config');
|
||||
|
||||
// create our server directory
|
||||
$serverDir = join('/', array($config->get('server_directory'), $params['serverName']));
|
||||
mkdir($serverDir);
|
||||
|
||||
// find the server JAR URL for the version
|
||||
$versionPageLink = 'https://minecraft.fandom.com/wiki/Java_Edition_' . $params['serverVersion'];
|
||||
$dom = new Document($versionPageLink, true);
|
||||
$infobox = $dom->find('.notaninfobox')[0];
|
||||
$serverLinkElem = $infobox->find("a:contains('Server')");
|
||||
$serverJarUrl = $serverLinkElem[0]->attr('href');
|
||||
|
||||
// grab the server JAR file
|
||||
$serverJarName = "server_" . $params['serverVersion'] . ".jar";
|
||||
$serverJarPath = join('/', array($serverDir, $serverJarName));
|
||||
file_put_contents($serverJarPath, file_get_contents($serverJarUrl));
|
||||
|
||||
// create the start.sh shell script
|
||||
$scriptFilePath = join('/', array($serverDir, 'start.sh'));
|
||||
$scriptFile = fopen($scriptFilePath, 'w');
|
||||
$scriptContent = "#!/bin/sh\n\ncd " . $serverDir . "\njava -Xmx2048M -Xms2048M -jar server_" . $params['serverVersion'] . ".jar nogui";
|
||||
fwrite($scriptFile, $scriptContent);
|
||||
fclose($scriptFile);
|
||||
chmod($scriptFilePath, 0755);
|
||||
|
||||
// save the current version to a text file - this will hopefully be temporary solution
|
||||
$versionFilePath = join('/', array($serverDir, 'current_version.txt'));
|
||||
$versionFile = fopen($versionFilePath, 'w');
|
||||
$versionContent = $params['serverVersion'];
|
||||
fwrite($versionFile, $versionContent);
|
||||
fclose($versionFile);
|
||||
chmod($versionFilePath, 0644);
|
||||
|
||||
// redirect the user back to the home page
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
// GET server status route
|
||||
public function getStatus(Request $request, Response $response, $args): Response {
|
||||
$config = $this->get('config');
|
||||
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
|
||||
|
||||
// check if the server exists - if not, return a false result
|
||||
if (!is_dir($serverDir)) {
|
||||
$response->getBody()->write(json_encode(array('exists' => false)));
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// create server object and pass info back to client as JSON data
|
||||
$server = new Server($serverDir);
|
||||
$serverData = [
|
||||
'exists' => true,
|
||||
'name' => $server->getName(),
|
||||
'version' => $server->getVersion(),
|
||||
'state' => $server->getState() ? 'Running' : 'Stopped',
|
||||
];
|
||||
|
||||
$response->getBody()->write(json_encode($serverData));
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
// GET server start route
|
||||
public function getStart(Request $request, Response $response, $args): Response {
|
||||
$config = $this->get('config');
|
||||
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
|
||||
|
||||
// create server object and start it
|
||||
$server = new Server($serverDir);
|
||||
$server->start();
|
||||
|
||||
// redirect the user back to the home page
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
// GET server stop route
|
||||
public function getStop(Request $request, Response $response, $args): Response {
|
||||
$config = $this->get('config');
|
||||
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
|
||||
|
||||
// create server object and start it
|
||||
$server = new Server($serverDir);
|
||||
$server->stop();
|
||||
|
||||
// redirect the user back to the home page
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
// GET edit server properties route
|
||||
public function getEdit(Request $request, Response $response, $args): Response {
|
||||
$config = $this->get('config');
|
||||
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
|
||||
|
||||
$server = new Server($serverDir);
|
||||
$data = array(
|
||||
'serverName' => $args['serverName'],
|
||||
'serverPort' => $server->getProperty('server-port'),
|
||||
);
|
||||
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'edit.twig', $data);
|
||||
}
|
||||
|
||||
// POST edit server properties route
|
||||
public function postEdit(Request $request, Response $response, $args): Response {
|
||||
// set up the POST parameters and grab our config
|
||||
$params = (array)$request->getParsedBody();
|
||||
$config = $this->get('config');
|
||||
$serverDir = join('/', array($config->get('server_directory'), $args['serverName']));
|
||||
|
||||
// create server object and save new value
|
||||
$server = new Server($serverDir);
|
||||
$server->updateProperty('server-port', $params['serverPort']);
|
||||
|
||||
// redirect the user back to the home page
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
}
|
111
src/Minecraft/Server.php
Normal file
111
src/Minecraft/Server.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\MCST\Minecraft;
|
||||
|
||||
class Server {
|
||||
|
||||
// server root directory
|
||||
private string $rootDir;
|
||||
// server name
|
||||
private string $serverName;
|
||||
// server Minecraft version
|
||||
private string $serverVersion;
|
||||
// PID file path
|
||||
private string $pidFilePath;
|
||||
// server run status
|
||||
private bool $state = false;
|
||||
// server config properties
|
||||
private ServerConfig $properties;
|
||||
|
||||
// class constructor
|
||||
public function __construct($dir) {
|
||||
$dirBits = explode('/', $dir);
|
||||
$this->rootDir = $dir;
|
||||
$this->serverName = $dirBits[count($dirBits) - 1];
|
||||
$this->pidFilePath = $this->rootDir . '/pid.txt';
|
||||
$this->properties = new ServerConfig($this->rootDir . '/server.properties');
|
||||
|
||||
// get server version
|
||||
$versionFile = join('/', array($this->rootDir, 'current_version.txt'));
|
||||
if (file_exists($versionFile)) {
|
||||
$this->serverVersion = file($versionFile)[0];
|
||||
}
|
||||
|
||||
// search for server PID file
|
||||
if (file_exists($this->pidFilePath)) {
|
||||
// get and search for PID
|
||||
$pid = trim($this->getPid());
|
||||
if (file_exists('/proc/' . $pid)) {
|
||||
// set server state to true since it is running
|
||||
$this->state = true;
|
||||
} else {
|
||||
// delete the PID file since it's no longer needed
|
||||
unlink($this->pidFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start the server
|
||||
public function start(): void {
|
||||
$command = $this->rootDir . '/start.sh > /dev/null 2>&1 & echo $!';
|
||||
exec($command, $output);
|
||||
// use lsof to find the PID of the server
|
||||
$pidResult = exec('lsof ' . $this->rootDir . '/server*.jar | grep .jar');
|
||||
$pid = preg_split('/\s+/', $pidResult, -1, PREG_SPLIT_NO_EMPTY)[1];
|
||||
|
||||
$pidFile = fopen($this->pidFilePath, 'w');
|
||||
fwrite($pidFile, $pid);
|
||||
fclose($pidFile);
|
||||
|
||||
// set server state to true
|
||||
$this->state = true;
|
||||
}
|
||||
|
||||
// stop the server
|
||||
public function stop(): void {
|
||||
$pid = $this->getPid();
|
||||
if ($pid != -1) {
|
||||
// kill the process gracefully
|
||||
exec('kill -15 ' . $pid);
|
||||
// set server state to false
|
||||
$this->state = false;
|
||||
}
|
||||
}
|
||||
|
||||
// get the server's PID
|
||||
private function getPid(): int {
|
||||
// search for server PID file
|
||||
if (file_exists($this->pidFilePath)) {
|
||||
// get and return server PID
|
||||
return trim(file($this->pidFilePath)[0]);
|
||||
}
|
||||
|
||||
// return a 'false' result
|
||||
return -1;
|
||||
}
|
||||
|
||||
// get property from server properties file
|
||||
public function getProperty(string $param) {
|
||||
return $this->properties->get($param);
|
||||
}
|
||||
|
||||
// update property in server properties file
|
||||
public function updateProperty(string $paramName, $paramValue) {
|
||||
$this->properties->update($paramName, $paramValue);
|
||||
}
|
||||
|
||||
// getters & setters
|
||||
public function getDirectory(): string {
|
||||
return $this->rootDir;
|
||||
}
|
||||
public function getName(): string {
|
||||
return $this->serverName;
|
||||
}
|
||||
public function getVersion(): string {
|
||||
return $this->serverVersion;
|
||||
}
|
||||
public function getState(): bool {
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
}
|
64
src/Minecraft/ServerConfig.php
Normal file
64
src/Minecraft/ServerConfig.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\MCST\Minecraft;
|
||||
|
||||
class ServerConfig {
|
||||
|
||||
// path to the server.properties file
|
||||
protected string $propertiesFile;
|
||||
// loaded properties
|
||||
protected array $config;
|
||||
|
||||
public function __construct($propertiesFile) {
|
||||
$this->propertiesFile = $propertiesFile; // save the properties file
|
||||
$this->config = array(); // initialize array
|
||||
|
||||
// parse config file
|
||||
$fileHandle = fopen($propertiesFile, 'r+');
|
||||
while (!feof($fileHandle)) {
|
||||
$line = fgets($fileHandle);
|
||||
$eqpos = strpos($line, '=');
|
||||
$parameterName = substr($line, 0, $eqpos);
|
||||
$parameterValue = trim(substr($line, ($eqpos + 1)));
|
||||
|
||||
// assign parameter values to the config array
|
||||
$this->config[$parameterName] = $parameterValue;
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $param) {
|
||||
return $this->config[$param];
|
||||
}
|
||||
|
||||
public function update(string $paramName, $paramValue) {
|
||||
// update the local variable value
|
||||
$this->config[$paramName] = $paramValue;
|
||||
|
||||
// update and save the server.properties file
|
||||
$newFileContent = '';
|
||||
$fileHandle = fopen($this->propertiesFile, 'r+');
|
||||
while (!feof($fileHandle)) {
|
||||
$line = fgets($fileHandle);
|
||||
|
||||
if (!str_starts_with($line, '#')) {
|
||||
$eqpos = strpos($line, '=');
|
||||
$parameterName = substr($line, 0, $eqpos);
|
||||
$parameterValue = substr($line, ($eqpos + 1));
|
||||
|
||||
// use the new value if it's being updated
|
||||
if ($parameterName == $paramName) {
|
||||
$parameterValue = $paramValue . "\n";
|
||||
}
|
||||
|
||||
$newFileContent .= $parameterName . '=' . $parameterValue;
|
||||
} else {
|
||||
$newFileContent .= $line;
|
||||
}
|
||||
}
|
||||
|
||||
// save the new properties file
|
||||
file_put_contents($this->propertiesFile, $newFileContent);
|
||||
fclose($fileHandle);
|
||||
}
|
||||
|
||||
}
|
35
src/app.php
Normal file
35
src/app.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use DI\Container;
|
||||
use Noodlehaus\Config;
|
||||
use Noodlehaus\Parser\Json;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Slim\Views\Twig;
|
||||
use Slim\Views\TwigMiddleware;
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Load app configuration
|
||||
$config = Config::load(__DIR__ . '/../conf/defaults.json');
|
||||
|
||||
// Create new container object and add our config object to it
|
||||
$container = new Container();
|
||||
$container->set('config', function () use ($config) {
|
||||
return $config;
|
||||
});
|
||||
|
||||
// Set container to create App with on AppFactory
|
||||
AppFactory::setContainer($container);
|
||||
$app = AppFactory::create();
|
||||
|
||||
// Add Error Handling Middleware
|
||||
$app->addErrorMiddleware(true, false, false);
|
||||
|
||||
// Create Twig
|
||||
$twig = Twig::create(__DIR__ . '/../views', ['cache' => false]);
|
||||
|
||||
// Add Twig-View Middleware
|
||||
$app->add(TwigMiddleware::create($app, $twig));
|
||||
|
||||
// load in route handlers
|
||||
require_once __DIR__ . '/routes.php';
|
@ -1,2 +0,0 @@
|
||||
pub mod new;
|
||||
pub mod start;
|
@ -1,37 +0,0 @@
|
||||
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(())
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
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
45
src/main.rs
@ -1,45 +0,0 @@
|
||||
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),
|
||||
};
|
||||
}
|
27
src/routes.php
Normal file
27
src/routes.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
use BitGoblin\MCST\Minecraft\Server;
|
||||
|
||||
// index GET route - this page should welcome the user and direct them to the available actions
|
||||
$app->get('/', '\\BitGoblin\\MCST\\Controllers\\HomeController:getIndex')->setName('index');
|
||||
|
||||
// create GET route - this page allows a user to create a new server instance
|
||||
$app->get('/create', '\\BitGoblin\\MCST\\Controllers\\ServerController:getCreate')->setName('create');
|
||||
// create POST route - processes the new server creation
|
||||
$app->post('/create', '\\BitGoblin\\MCST\\Controllers\\ServerController:postCreate');
|
||||
|
||||
// server status route
|
||||
$app->get('/server/{serverName}/status', '\\BitGoblin\\MCST\\Controllers\\ServerController:getStatus')->setName('server.status');
|
||||
|
||||
// server start route
|
||||
$app->get('/server/{serverName}/start', '\\BitGoblin\\MCST\\Controllers\\ServerController:getStart')->setName('server.start');
|
||||
// server stop route
|
||||
$app->get('/server/{serverName}/stop', '\\BitGoblin\\MCST\\Controllers\\ServerController:getStop')->setName('server.stop');
|
||||
|
||||
// edit GET route - this page allows a user to edit a server's properties
|
||||
$app->get('/server/{serverName}/edit', '\\BitGoblin\\MCST\\Controllers\\ServerController:getEdit')->setName('edit');
|
||||
// edit POST route - processes the edits a server's properties file
|
||||
$app->post('/server/{serverName}/edit', '\\BitGoblin\\MCST\\Controllers\\ServerController:postEdit');
|
40
views/create.twig
Normal file
40
views/create.twig
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- page header -->
|
||||
<header class="row">
|
||||
<div class="columns twelve">
|
||||
<h1>Create new server</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- create server form -->
|
||||
<section class="row">
|
||||
<div class="columns twelve">
|
||||
<form action="/create" method="POST">
|
||||
<div class="row">
|
||||
<div class="columns six">
|
||||
<label for="serverName">Server name:</label>
|
||||
<input id="serverName" name="serverName" class="u-full-width" type="text" placeholder="MyServer" required>
|
||||
</div>
|
||||
|
||||
<div class="columns six">
|
||||
<label for="serverVersion">Minecraft version:</label>
|
||||
<input id="serverVersion" name="serverVersion" class="u-full-width" type="text" placeholder="1.19.2" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input id="createSubmit" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- lower navigation -->
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<p><a href="/">Back</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
35
views/edit.twig
Normal file
35
views/edit.twig
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- page header -->
|
||||
<header class="row">
|
||||
<div class="columns twelve">
|
||||
<h1>Edit server: {{ serverName }}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- create server form -->
|
||||
<section class="row">
|
||||
<div class="columns twelve">
|
||||
<form action="/server/{{ serverName }}/edit" method="POST">
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<label for="serverPort">Server port:</label>
|
||||
<input id="serverPort" name="serverPort" class="u-full-width" type="number" placeholder="25565" required value={{ serverPort }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input id="editSubmit" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- lower navigation -->
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<p><a href="/">Back</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
50
views/index.twig
Normal file
50
views/index.twig
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- page header -->
|
||||
<header class="row">
|
||||
<div class="columns twelve">
|
||||
<h1>Welcome to MCST!</h1>
|
||||
<p>Using MCST you can easily manage your Minecraft: Java Edition servers.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- list of servers -->
|
||||
<section class="row">
|
||||
<div class="columns twelve">
|
||||
<h3>List of servers:</h3>
|
||||
{% if servers|length < 1 %}
|
||||
<p>There are currently no servers registered.</p>
|
||||
{% else %}
|
||||
<table class="u-full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server name</th>
|
||||
<th>Minecraft version</th>
|
||||
<th>Port</th>
|
||||
<th>State</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in servers %}
|
||||
<tr class="serverItem" data-server-name="{{ m.getName() }}">
|
||||
<td class="serverName">{{ m.getName() }}</td>
|
||||
<td class="serverVersion">{{ m.getVersion() }}</td>
|
||||
<td class="serverPort">{{ m.getProperty('server-port') }}</td>
|
||||
<td class="serverState">{{ m.getState() ? 'Running' : 'Stopped' }}</td>
|
||||
<td>
|
||||
<a href="/server/{{ m.getName() }}/start"><i class="fa-solid fa-play"></i></a>
|
||||
<a href="/server/{{ m.getName() }}/stop"><i class="fa-solid fa-stop"></i></a>
|
||||
<a href="/server/{{ m.getName() }}/edit"><i class="fa-solid fa-pen-to-square"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
32
views/layout.twig
Normal file
32
views/layout.twig
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Minecraft Server Tool</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
|
||||
<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>
|
||||
<script src="/js/drake.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- global navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-left">
|
||||
<ul>
|
||||
<li class="menu-text">MCST</li>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/create">Create</a></li>
|
||||
<li><a href="/status">Status</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- main content -->
|
||||
<div id="main-content" class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user