📂 File Browser

serversdashboard/serversdashboard/lib
🌙 Dark Mode
🎯 Quick Launch:

📁 Directories

📄 Files

🐘 clientid.php
▶ Open 📄 View Source
🐘 net.php
▶ Open 📄 View Source
🐘 rcon.php
▶ Open 📄 View Source
🐘 servers.php
▶ Open 📄 View Source
🐘 snbt.php
▶ Open 📄 View Source
🐘 tmux.php
▶ Open 📄 View Source
🐘 users.php
▶ Open 📄 View Source

📄 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 a systemd unit exists, check it.
    if ($unit !== null) {
        exec('systemctl list-unit-files ' . escapeshellarg($unit) . ' 2>/dev/null', $out, $code);
        if ($code === 0 && !empty($out)) {
            exec('systemctl is-enabled ' . escapeshellarg($unit) . ' 2>/dev/null', $_, $code2);
            return $code2 === 0;
        }
    }

    // Otherwise, check the crontab.
    return crontab_has_reboot($root, $name, $cfg);
}

/**
 * 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 a systemd unit exists, use it.
    if ($unit !== null) {
        exec('systemctl list-unit-files ' . escapeshellarg($unit) . ' 2>/dev/null', $out, $code);
        if ($code === 0 && !empty($out)) {
            exec('sudo -n systemctl enable ' . escapeshellarg($unit) . ' 2>&1', $out2, $code2);
            return [$code2 === 0, $code2 === 0 ? 'Enabled via systemd' : implode("\n", $out2)];
        }
    }

    // Otherwise, write a @reboot line into the user's crontab.
    return crontab_add_reboot($root, $name, $cfg);
}

function server_disable(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);
        if ($code === 0 && !empty($out)) {
            exec('sudo -n systemctl disable ' . escapeshellarg($unit) . ' 2>&1', $out2, $code2);
            return [$code2 === 0, $code2 === 0 ? 'Disabled via systemd' : implode("\n", $out2)];
        }
    }

    return crontab_remove_reboot($root, $name, $cfg);
}

function crontab_reboot_line(string $root, string $name, array $cfg): string {
    $path = server_path($root, $name);
    $session = tmux_session_name($cfg['tmux_template'] ?? '%s', $name);
    $script = $cfg['start_script'] ?? 'start.sh';
    $inner = 'cd ' . $path . ' && ./' . $script;
    // The whole inner command is passed to tmux as a single argument.
    // Escape single quotes by closing, escaping, reopening.
    $innerEsc = str_replace("'", "'\\''", $inner);
    return "@reboot tmux new-session -d -s {$session} '{$innerEsc}'";
}

function crontab_read(): array {
    $out = [];
    exec('crontab -l 2>/dev/null', $out, $code);
    return $code === 0 ? $out : [];
}

function crontab_write(array $lines): bool {
    $content = implode("\n", $lines) . "\n";
    $tmp = tempnam(sys_get_temp_dir(), 'cron');
    file_put_contents($tmp, $content);
    exec('crontab ' . escapeshellarg($tmp) . ' 2>&1', $out, $code);
    @unlink($tmp);
    return $code === 0;
}

function crontab_add_reboot(string $root, string $name, array $cfg): array {
    $line = crontab_reboot_line($root, $name, $cfg);
    $lines = crontab_read();

    foreach ($lines as $existing) {
        if (trim($existing) === trim($line)) {
            return [true, 'Already in crontab'];
        }
    }

    $lines[] = $line;
    if (crontab_write($lines)) {
        return [true, 'Added @reboot entry to crontab'];
    }
    return [false, 'Failed to write crontab'];
}

function crontab_remove_reboot(string $root, string $name, array $cfg): array {
    $line = crontab_reboot_line($root, $name, $cfg);
    $lines = crontab_read();

    $before = count($lines);
    $lines = array_values(array_filter($lines, fn($l) => trim($l) !== trim($line)));

    if (count($lines) === $before) {
        return [true, 'Not in crontab'];
    }
    if (crontab_write($lines)) {
        return [true, 'Removed @reboot entry from crontab'];
    }
    return [false, 'Failed to write crontab'];
}

function crontab_has_reboot(string $root, string $name, array $cfg): bool {
    $line = crontab_reboot_line($root, $name, $cfg);
    foreach (crontab_read() as $existing) {
        if (trim($existing) === trim($line)) return true;
    }
    return false;
}


/**
 * 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