📄 Source: servers.php
<?php
/**
* Server discovery and control.
*
* A "server" is any directory under SERVER_ROOT that looks like a
* Minecraft server (contains server.properties, or a start.sh).
*
* Starting a server: prefers systemd if a unit exists, otherwise launches
* the start.sh script inside a named tmux session so that the console
* stays attached and the dashboard can capture it.
*
* Stopping a server: sends "stop" into the tmux console and waits for the
* world to save before forcing the session closed.
*/
require_once __DIR__ . '/tmux.php';
require_once __DIR__ . '/rcon.php';
function servers_discover(string $root): array {
if (!is_dir($root)) {
return [];
}
$out = [];
foreach (scandir($root) as $entry) {
if ($entry === '.' || $entry === '..') continue;
$path = rtrim($root, '/') . '/' . $entry;
if (!is_dir($path)) continue;
$looks_like_mc = is_file($path . '/server.properties')
|| is_file($path . '/start.sh')
|| is_file($path . '/eula.txt');
if ($looks_like_mc) {
$out[] = $entry;
}
}
sort($out);
return $out;
}
function server_path(string $root, string $name): string {
return rtrim($root, '/') . '/' . $name;
}
/**
* Build the systemd unit name for a server, or null if systemd is disabled.
*/
function server_unit(?string $template, string $name): ?string {
if ($template === null || $template === '') return null;
return str_replace('%s', $name, $template);
}
/**
* Return one of: running, stopped, unknown.
* Tries systemd first, then tmux.
*/
function server_state(string $root, string $name, array $cfg): string {
$unit = server_unit($cfg['systemd_template'] ?? null, $name);
if ($unit !== null) {
exec('systemctl is-active ' . escapeshellarg($unit) . ' 2>/dev/null', $_, $code);
if ($code === 0) return 'running';
// If the unit doesn't exist, is-active returns non-zero too,
// so fall through to tmux.
}
$session = tmux_session_name($cfg['tmux_template'] ?? '%s', $name);
if (tmux_has_session($session)) return 'running';
return 'stopped';
}
function server_autostart_enabled(string $root, string $name, array $cfg): ?bool {
$unit = server_unit($cfg['systemd_template'] ?? null, $name);
if ($unit === null) return null;
exec('systemctl is-enabled ' . escapeshellarg($unit) . ' 2>/dev/null', $_, $code);
return $code === 0;
}
/**
* Start a server.
*
* 1. If a systemd unit exists and is enabled, use systemctl.
* 2. Otherwise, launch start.sh inside a named tmux session so the
* console stays attached and can be captured by the dashboard.
*
* Returns [ok, message].
*/
function server_start(string $root, string $name, array $cfg): array {
$path = server_path($root, $name);
$unit = server_unit($cfg['systemd_template'] ?? null, $name);
// 1. Prefer systemd if the unit actually exists.
if ($unit !== null) {
// Only try systemctl if the unit is known to the system.
exec('systemctl list-unit-files ' . escapeshellarg($unit) . ' 2>/dev/null', $out, $code);
$unitExists = ($code === 0 && !empty($out));
if ($unitExists) {
exec('sudo -n systemctl start ' . escapeshellarg($unit) . ' 2>&1', $out2, $code2);
if ($code2 === 0) {
return [true, 'Started via systemd (' . $unit . ')'];
}
// fall through to tmux if systemctl failed
}
}
// 2. Fall back to launching inside a named tmux session.
$session = tmux_session_name($cfg['tmux_template'] ?? '%s', $name);
if (tmux_has_session($session)) {
return [false, "tmux session '$session' already exists"];
}
$script = $path . '/' . ($cfg['start_script'] ?? 'start.sh');
if (!is_file($script)) {
return [false, 'No systemd unit and no start.sh found in ' . $path];
}
// Make sure start.sh is executable (harmless if already +x).
exec('chmod +x ' . escapeshellarg($script) . ' 2>&1');
// Launch the server inside a detached tmux session.
// The session runs: cd <server dir> && ./start.sh
$inner = 'cd ' . escapeshellarg($path) . ' && ./' . escapeshellarg(basename($script));
$cmd = 'tmux new-session -d -s ' . escapeshellarg($session)
. ' ' . escapeshellarg($inner);
exec($cmd, $out, $code);
if ($code === 0) {
return [true, "Started in tmux session '$session'"];
}
return [false, 'tmux new-session failed: ' . implode("\n", $out)];
}
/**
* Stop a server gracefully.
*
* 1. If a systemd unit exists, use systemctl stop.
* 2. Otherwise, send "stop" into the tmux console and wait up to 30s
* for the session to close on its own (so the world saves).
* 3. If still alive after 30s, force-kill the tmux session.
*/
function server_stop(string $root, string $name, array $cfg): array {
$unit = server_unit($cfg['systemd_template'] ?? null, $name);
if ($unit !== null) {
exec('systemctl list-unit-files ' . escapeshellarg($unit) . ' 2>/dev/null', $out, $code);
$unitExists = ($code === 0 && !empty($out));
if ($unitExists) {
exec('sudo -n systemctl stop ' . escapeshellarg($unit) . ' 2>&1', $out2, $code2);
if ($code2 === 0) return [true, 'Stopped via systemd (' . $unit . ')'];
}
}
$session = tmux_session_name($cfg['tmux_template'] ?? '%s', $name);
if (tmux_has_session($session)) {
// Send the graceful stop command into the server console.
tmux_send_command($session, 'stop');
// Wait up to 30 seconds for the session to close on its own.
for ($i = 0; $i < 30; $i++) {
sleep(1);
if (!tmux_has_session($session)) {
return [true, 'Stopped gracefully (world saved)'];
}
}
// Still alive after 30s — force kill as a last resort.
exec('tmux kill-session -t ' . escapeshellarg($session) . ' 2>&1', $out, $code);
return [true, 'Stopped (forced after 30s timeout)'];
}
$path = server_path($root, $name);
$script = $path . '/' . ($cfg['stop_script'] ?? 'stop.sh');
if (is_file($script)) {
exec('cd ' . escapeshellarg($path) . ' && bash ' . escapeshellarg($script) . ' 2>&1', $out, $code);
return [$code === 0, $code === 0 ? 'Stopped via stop.sh' : 'stop.sh failed'];
}
return [false, 'Nothing running to stop, and no stop.sh found'];
}
function server_enable(string $root, string $name, array $cfg): array {
$unit = server_unit($cfg['systemd_template'] ?? null, $name);
if ($unit === null) return [false, 'systemd disabled in config'];
exec('sudo -n systemctl enable ' . escapeshellarg($unit) . ' 2>&1', $out, $code);
return [$code === 0, $code === 0 ? 'Enabled' : implode("\n", $out)];
}
function server_disable(string $root, string $name, array $cfg): array {
$unit = server_unit($cfg['systemd_template'] ?? null, $name);
if ($unit === null) return [false, 'systemd disabled in config'];
exec('sudo -n systemctl disable ' . escapeshellarg($unit) . ' 2>&1', $out, $code);
return [$code === 0, $code === 0 ? 'Disabled' : implode("\n", $out)];
}
/**
* Console output: try tmux first, then the log file.
*/
function server_console(string $root, string $name, array $cfg): string {
$session = tmux_session_name($cfg['tmux_template'] ?? '%s', $name);
$tmuxOut = tmux_capture($session, (int)($cfg['console_lines'] ?? 300));
if ($tmuxOut !== null && trim($tmuxOut) !== '') {
return $tmuxOut;
}
$log = server_path($root, $name) . '/' . ($cfg['log_file'] ?? 'logs/latest.log');
if (is_file($log)) {
$lines = (int)($cfg['console_lines'] ?? 300);
$cmd = 'tail -n ' . $lines . ' ' . escapeshellarg($log) . ' 2>&1';
return (string)shell_exec($cmd);
}
return '(no console source available)';
}
← Back