Fix server management issues and improve overall stability

Major server management fixes:
- Replace Flatpak-specific pkill with universal process tree termination using pstree + process.kill()
- Fix signal format errors (SIGTERM/SIGKILL instead of TERM/KILL strings)
- Add 5-second cooldown after server stop to prevent race conditions with external detection
- Enable Stop Server button for external servers in UI
- Implement proper timeout handling with process tree killing

ContentDB improvements:
- Fix download retry logic and "closed" error by preventing concurrent zip extraction
- Implement smart root directory detection and stripping during package extraction
- Add game-specific timeout handling (8s for VoxeLibre vs 3s for simple games)

World creation fixes:
- Make world creation asynchronous to prevent browser hangs
- Add WebSocket notifications for world creation completion status

Other improvements:
- Remove excessive debug logging
- Improve error handling and user feedback throughout the application
- Clean up temporary files and unnecessary logging

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Nathan Schneider
2025-08-24 19:17:38 -06:00
parent 3aed09b60f
commit 2d3b1166fe
15 changed files with 851 additions and 536 deletions

View File

@@ -69,16 +69,6 @@ router.get('/server/status', async (req, res) => {
}
const isExternal = serverManager.serverProcess?.external || false;
console.log('API: serverManager.serverProcess =', serverManager.serverProcess);
console.log('API: isExternal =', isExternal);
console.log('API endpoint returning status:', {
isRunning: status.isRunning,
players: playerList.length, // Use the actual detected player count
playerNames: playerList.map(p => p.name),
statusText: status.isRunning ? 'running' : 'stopped',
isExternal: isExternal
});
res.json({
...status,
@@ -531,4 +521,4 @@ module.exports = {
setSocketIO,
serverManager,
configManager
};
};

View File

@@ -2,6 +2,8 @@ const express = require('express');
const AuthManager = require('../utils/auth');
const { redirectIfAuthenticated } = require('../middleware/auth');
const securityLogger = require('../utils/security-logger');
const paths = require('../utils/paths');
const appConfig = require('../utils/app-config');
const router = express.Router();
const authManager = new AuthManager();
@@ -50,10 +52,15 @@ router.get('/register', redirectIfAuthenticated, async (req, res) => {
});
}
// Detect available Luanti data directories
const detectedDirectories = paths.detectLuantiDataDirectories();
res.render('auth/register', {
title: 'Setup Administrator Account',
isFirstUser: isFirstUser,
currentPage: 'register'
currentPage: 'register',
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
} catch (error) {
console.error('Error checking first user:', error);
@@ -119,29 +126,74 @@ router.post('/register', redirectIfAuthenticated, async (req, res) => {
});
}
const { username, password, confirmPassword } = req.body;
const { username, password, confirmPassword, dataDirectory, customDataDirectory } = req.body;
// Validate inputs
if (!username || !password || !confirmPassword) {
const detectedDirectories = paths.detectLuantiDataDirectories();
return res.render('auth/register', {
title: 'Setup Administrator Account',
error: 'All fields are required',
isFirstUser: true,
currentPage: 'register',
formData: { username }
formData: { username },
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
if (password !== confirmPassword) {
const detectedDirectories = paths.detectLuantiDataDirectories();
return res.render('auth/register', {
title: 'Setup Administrator Account',
error: 'Passwords do not match',
isFirstUser: true,
currentPage: 'register',
formData: { username }
formData: { username },
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
// Handle data directory selection
let selectedDataDir = dataDirectory;
if (dataDirectory === 'custom' && customDataDirectory) {
selectedDataDir = customDataDirectory;
}
// Validate the selected data directory
if (selectedDataDir && selectedDataDir !== 'custom') {
try {
// Ensure the directory exists or can be created
const fs = require('fs');
if (!fs.existsSync(selectedDataDir)) {
await fs.promises.mkdir(selectedDataDir, { recursive: true });
}
// Save the data directory to app config
await appConfig.load();
appConfig.setDataDirectory(selectedDataDir);
await appConfig.save();
// Update paths to use the new directory
paths.setDataDirectory(selectedDataDir);
console.log('Data directory set to:', selectedDataDir);
} catch (error) {
console.error('Error setting data directory:', error);
const detectedDirectories = paths.detectLuantiDataDirectories();
return res.render('auth/register', {
title: 'Setup Administrator Account',
error: `Invalid data directory: ${error.message}`,
isFirstUser: true,
currentPage: 'register',
formData: { username },
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
}
const user = await authManager.createUser(username, password);
// Create session for new user
@@ -157,6 +209,7 @@ router.post('/register', redirectIfAuthenticated, async (req, res) => {
} catch (error) {
console.error('Registration error:', error);
const detectedDirectories = paths.detectLuantiDataDirectories();
res.render('auth/register', {
title: 'Register',
error: error.message,
@@ -164,7 +217,9 @@ router.post('/register', redirectIfAuthenticated, async (req, res) => {
currentPage: 'register',
formData: {
username: req.body.username
}
},
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
});

View File

@@ -4,6 +4,7 @@ const fs = require('fs').promises;
const paths = require('../utils/paths');
const ConfigParser = require('../utils/config-parser');
const appConfig = require('../utils/app-config');
const PackageRegistry = require('../utils/package-registry');
const router = express.Router();
@@ -170,8 +171,11 @@ router.post('/update', async (req, res) => {
if (newDataDir && newDataDir !== appConfig.getDataDirectory()) {
try {
await appConfig.setDataDirectory(newDataDir);
// Update paths to use new directory
await paths.initialize();
// Force reload paths to use new directory
await paths.forceReload();
// Reinitialize package registry to use new directory
const packageRegistry = new PackageRegistry();
await packageRegistry.reinitialize();
} catch (error) {
throw new Error(`Failed to update data directory: ${error.message}`);
}
@@ -286,6 +290,53 @@ router.get('/api/schema', (req, res) => {
});
// Get current configuration (API)
// Change data directory only
router.post('/change-data-directory', async (req, res) => {
try {
const { newDataDirectory } = req.body;
if (!newDataDirectory || !newDataDirectory.trim()) {
return res.status(400).json({
success: false,
error: 'Data directory path is required'
});
}
const newDataDir = newDataDirectory.trim();
const currentDataDir = appConfig.getDataDirectory();
if (newDataDir === currentDataDir) {
return res.json({
success: true,
message: 'Data directory is already set to this path',
dataDirectory: currentDataDir
});
}
// Validate and set new data directory
await appConfig.load();
await appConfig.setDataDirectory(newDataDir);
await appConfig.save();
// Update paths to use new directory
await paths.initialize();
res.json({
success: true,
message: 'Data directory updated successfully. Please restart the application for all changes to take effect.',
dataDirectory: newDataDir,
previousDirectory: currentDataDir
});
} catch (error) {
console.error('Error changing data directory:', error);
res.status(500).json({
success: false,
error: `Failed to change data directory: ${error.message}`
});
}
});
router.get('/api/current', async (req, res) => {
try {
const config = await ConfigParser.parseConfig(paths.configFile);

View File

@@ -12,12 +12,14 @@ const router = express.Router();
const contentdb = new ContentDBClient();
const packageRegistry = new PackageRegistry();
// Initialize package registry
// Initialize package registry - will be reinitialized when paths change
packageRegistry.init().catch(console.error);
// Main Extensions page - shows installed content and installer
router.get('/', async (req, res) => {
try {
// Ensure we're using the current data directory
await paths.initialize();
paths.ensureDirectories();
// Get installed packages from registry (games, mods, texture packs)

View File

@@ -6,6 +6,7 @@ const { promisify } = require('util');
const paths = require('../utils/paths');
const ConfigParser = require('../utils/config-parser');
const serverManager = require('../utils/shared-server-manager');
const router = express.Router();
@@ -39,11 +40,15 @@ router.get('/', async (req, res) => {
db.close();
} catch (dbError) {}
const gameid = config.gameid || 'minetest_game';
const gameTitle = await paths.getGameDisplayName(gameid);
worlds.push({
name: worldDir,
displayName: config.server_name || worldDir,
description: config.server_description || '',
gameid: config.gameid || 'minetest_game',
gameid: gameid,
gameTitle: gameTitle,
creativeMode: config.creative_mode || false,
enableDamage: config.enable_damage !== false,
enablePvp: config.enable_pvp !== false,
@@ -128,131 +133,42 @@ router.post('/create', async (req, res) => {
console.log('Starting world creation for:', name, 'with gameid:', gameid);
// Create the world directory - Luanti will initialize it when the server starts
await fs.mkdir(worldPath, { recursive: true });
console.log('Created world directory:', worldPath);
// Create a basic world.mt file with the correct game ID
const worldConfig = `enable_damage = true
creative_mode = false
mod_storage_backend = sqlite3
auth_backend = sqlite3
player_backend = sqlite3
backend = sqlite3
gameid = ${gameid || 'minetest_game'}
world_name = ${name}
`;
const worldConfigPath = path.join(worldPath, 'world.mt');
await fs.writeFile(worldConfigPath, worldConfig, 'utf8');
console.log('Created world.mt with gameid:', gameid || 'minetest_game');
// Create essential database files with proper schema
const sqlite3 = require('sqlite3');
// Create players database with correct schema
const playersDbPath = path.join(worldPath, 'players.sqlite');
await new Promise((resolve, reject) => {
const playersDb = new sqlite3.Database(playersDbPath, (err) => {
if (err) reject(err);
else {
playersDb.serialize(() => {
playersDb.exec(`CREATE TABLE IF NOT EXISTS player (
name TEXT PRIMARY KEY,
pitch REAL,
yaw REAL,
posX REAL,
posY REAL,
posZ REAL,
hp INTEGER,
breath INTEGER,
creation_date INTEGER,
modification_date INTEGER,
privs TEXT
)`, (err) => {
if (err) {
console.error('Error creating player table:', err);
reject(err);
} else {
console.log('Created player table in players.sqlite');
playersDb.close((closeErr) => {
if (closeErr) reject(closeErr);
else resolve();
});
}
});
// Start world creation asynchronously and respond immediately
serverManager.createWorld(name, gameid || 'minetest_game')
.then(() => {
console.log('Successfully created world using Luanti:', name);
// Notify clients via WebSocket
const io = req.app.get('socketio');
if (io) {
io.emit('worldCreated', {
worldName: name,
gameId: gameid || 'minetest_game',
success: true
});
}
})
.catch((error) => {
console.error('Error creating world:', error);
// Notify clients via WebSocket about the error
const io = req.app.get('socketio');
if (io) {
io.emit('worldCreated', {
worldName: name,
gameId: gameid || 'minetest_game',
success: false,
error: error.message
});
}
});
});
// Create other essential databases
const mapDbPath = path.join(worldPath, 'map.sqlite');
await new Promise((resolve, reject) => {
const mapDb = new sqlite3.Database(mapDbPath, (err) => {
if (err) reject(err);
else {
mapDb.serialize(() => {
mapDb.exec(`CREATE TABLE IF NOT EXISTS blocks (
x INTEGER,
y INTEGER,
z INTEGER,
data BLOB NOT NULL,
PRIMARY KEY (x, z, y)
)`, (err) => {
if (err) {
console.error('Error creating blocks table:', err);
reject(err);
} else {
console.log('Created blocks table in map.sqlite');
mapDb.close((closeErr) => {
if (closeErr) reject(closeErr);
else resolve();
});
}
});
});
}
});
});
const modStorageDbPath = path.join(worldPath, 'mod_storage.sqlite');
await new Promise((resolve, reject) => {
const modDb = new sqlite3.Database(modStorageDbPath, (err) => {
if (err) reject(err);
else {
modDb.serialize(() => {
modDb.exec(`CREATE TABLE IF NOT EXISTS entries (
modname TEXT NOT NULL,
key BLOB NOT NULL,
value BLOB NOT NULL,
PRIMARY KEY (modname, key)
)`, (err) => {
if (err) {
console.error('Error creating entries table:', err);
reject(err);
} else {
console.log('Created entries table in mod_storage.sqlite');
modDb.close((closeErr) => {
if (closeErr) reject(closeErr);
else resolve();
});
}
});
});
}
});
});
console.log('Created essential database files with proper schema');
res.redirect('/worlds?created=' + encodeURIComponent(name));
// Respond immediately with a "creating" status
res.redirect('/worlds?creating=' + encodeURIComponent(name));
} catch (error) {
console.error('Error creating world:', error);
console.error('Error starting world creation:', error);
res.status(500).render('worlds/new', {
title: 'Create World',
currentPage: 'worlds',
error: 'Failed to create world: ' + error.message,
error: 'Failed to start world creation: ' + error.message,
formData: req.body
});
}