Files
LuHost/utils/package-registry.js
Nathan Schneider 2d3b1166fe 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>
2025-08-24 19:17:38 -06:00

282 lines
7.4 KiB
JavaScript

const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs').promises;
class PackageRegistry {
constructor(dbPath = null) {
// If no dbPath provided, we'll set it during init based on current data directory
this.dbPath = dbPath;
this.db = null;
}
async init() {
// Set database path based on current data directory if not already set
if (!this.dbPath) {
const paths = require('./paths');
await paths.initialize();
this.dbPath = path.join(paths.minetestDir, 'luhost_packages.db');
}
// Ensure data directory exists
const dir = path.dirname(this.dbPath);
await fs.mkdir(dir, { recursive: true });
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database(this.dbPath, (err) => {
if (err) {
reject(err);
} else {
this.createTables().then(resolve).catch(reject);
}
});
});
}
async reinitialize() {
// Close existing database connection
if (this.db) {
await new Promise((resolve) => {
this.db.close((err) => {
if (err) console.error('Error closing database:', err);
resolve();
});
});
}
// Clear the path so it gets recalculated
this.dbPath = null;
// Reinitialize with new path
return this.init();
}
async createTables() {
const sql = `
CREATE TABLE IF NOT EXISTS installed_packages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author TEXT NOT NULL,
name TEXT NOT NULL,
version TEXT,
release_id TEXT,
install_location TEXT NOT NULL, -- 'global' or 'world:worldname'
install_path TEXT NOT NULL,
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
contentdb_url TEXT,
package_type TEXT, -- 'mod', 'game', 'txp'
title TEXT,
short_description TEXT,
dependencies TEXT, -- JSON string of dependencies
UNIQUE(author, name, install_location)
);
CREATE INDEX IF NOT EXISTS idx_package_location ON installed_packages(install_location);
CREATE INDEX IF NOT EXISTS idx_package_name ON installed_packages(author, name);
`;
return new Promise((resolve, reject) => {
this.db.exec(sql, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async recordInstallation(packageInfo) {
const {
author,
name,
version,
releaseId,
installLocation,
installPath,
contentdbUrl,
packageType,
title,
shortDescription,
dependencies = []
} = packageInfo;
const sql = `
INSERT OR REPLACE INTO installed_packages
(author, name, version, release_id, install_location, install_path,
contentdb_url, package_type, title, short_description, dependencies, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`;
const dependenciesJson = JSON.stringify(dependencies);
return new Promise((resolve, reject) => {
this.db.run(sql, [
author, name, version, releaseId, installLocation, installPath,
contentdbUrl, packageType, title, shortDescription, dependenciesJson
], function(err) {
if (err) {
reject(err);
} else {
resolve({ id: this.lastID, changes: this.changes });
}
});
});
}
async getInstalledPackages(location = null) {
let sql = 'SELECT * FROM installed_packages';
const params = [];
if (location) {
if (location === 'global') {
sql += ' WHERE install_location = ?';
params.push('global');
} else if (location.startsWith('world:')) {
sql += ' WHERE install_location = ?';
params.push(location);
}
}
sql += ' ORDER BY installed_at DESC';
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
reject(err);
} else {
// Parse dependencies JSON for each row
const packages = rows.map(row => ({
...row,
dependencies: row.dependencies ? JSON.parse(row.dependencies) : []
}));
resolve(packages);
}
});
});
}
async getInstalledPackage(author, name, location) {
const sql = `
SELECT * FROM installed_packages
WHERE author = ? AND name = ? AND install_location = ?
`;
return new Promise((resolve, reject) => {
this.db.get(sql, [author, name, location], (err, row) => {
if (err) {
reject(err);
} else if (row) {
row.dependencies = row.dependencies ? JSON.parse(row.dependencies) : [];
resolve(row);
} else {
resolve(null);
}
});
});
}
async isPackageInstalled(author, name, location) {
const packageInfo = await this.getInstalledPackage(author, name, location);
return packageInfo !== null;
}
async removePackage(author, name, location) {
const sql = `
DELETE FROM installed_packages
WHERE author = ? AND name = ? AND install_location = ?
`;
return new Promise((resolve, reject) => {
this.db.run(sql, [author, name, location], function(err) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes });
}
});
});
}
async getPackagesByWorld(worldName) {
return this.getInstalledPackages(`world:${worldName}`);
}
async getGlobalPackages() {
return this.getInstalledPackages('global');
}
async getAllInstallations() {
return this.getInstalledPackages();
}
async updatePackageInfo(author, name, location, updates) {
const setClause = [];
const params = [];
for (const [key, value] of Object.entries(updates)) {
if (key === 'dependencies' && Array.isArray(value)) {
setClause.push(`${key} = ?`);
params.push(JSON.stringify(value));
} else {
setClause.push(`${key} = ?`);
params.push(value);
}
}
setClause.push('updated_at = CURRENT_TIMESTAMP');
const sql = `
UPDATE installed_packages
SET ${setClause.join(', ')}
WHERE author = ? AND name = ? AND install_location = ?
`;
params.push(author, name, location);
return new Promise((resolve, reject) => {
this.db.run(sql, params, function(err) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes });
}
});
});
}
async getStatistics() {
const sql = `
SELECT
COUNT(*) as total_packages,
COUNT(CASE WHEN install_location = 'global' THEN 1 END) as global_packages,
COUNT(CASE WHEN install_location LIKE 'world:%' THEN 1 END) as world_packages,
COUNT(DISTINCT CASE WHEN install_location LIKE 'world:%' THEN install_location END) as worlds_with_packages
FROM installed_packages
`;
return new Promise((resolve, reject) => {
this.db.get(sql, [], (err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
}
async close() {
if (this.db) {
return new Promise((resolve) => {
this.db.close((err) => {
if (err) {
console.error('Error closing database:', err);
}
resolve();
});
});
}
}
}
module.exports = PackageRegistry;