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:
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -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()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -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);
|
||||
|
@@ -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)
|
||||
|
152
routes/worlds.js
152
routes/worlds.js
@@ -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
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user