@woopsy/mcpanel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/commands/commandRouter.js +525 -0
- package/dist/config/configManager.js +193 -0
- package/dist/index.js +776 -0
- package/dist/managers/backupManager.js +207 -0
- package/dist/managers/playitManager.js +450 -0
- package/dist/managers/serverManager.js +243 -0
- package/dist/services/downloadService.js +176 -0
- package/dist/services/processManager.js +242 -0
- package/dist/utils/colors.js +73 -0
- package/dist/utils/helpers.js +365 -0
- package/dist/utils/logger.js +85 -0
- package/package.json +67 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ServerManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const helpers_1 = require("../utils/helpers");
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
class ServerManager {
|
|
42
|
+
configManager;
|
|
43
|
+
constructor(configManager) {
|
|
44
|
+
this.configManager = configManager;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Helper to ensure server name is safe for directories (alphanumeric, dashes, underscores)
|
|
48
|
+
*/
|
|
49
|
+
cleanServerName(name) {
|
|
50
|
+
return name.replace(/[^a-zA-Z0-9_\-]/g, '');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Inspects a server folder and best-effort detects the server software
|
|
54
|
+
* (Paper, Fabric, Purpur, Vanilla, ...) and the Minecraft version.
|
|
55
|
+
*/
|
|
56
|
+
detectServerInfo(dir) {
|
|
57
|
+
let software = 'Vanilla';
|
|
58
|
+
let version = 'Unknown';
|
|
59
|
+
let files = [];
|
|
60
|
+
try {
|
|
61
|
+
files = fs.readdirSync(dir);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { software, version };
|
|
65
|
+
}
|
|
66
|
+
const lowerFiles = files.map(f => f.toLowerCase());
|
|
67
|
+
const jars = lowerFiles.filter(f => f.endsWith('.jar'));
|
|
68
|
+
const jarBlob = jars.join(' ');
|
|
69
|
+
// --- Software detection ---
|
|
70
|
+
const hasFabric = lowerFiles.includes('fabric-server-launch.jar') ||
|
|
71
|
+
lowerFiles.includes('fabric-server-launcher.properties') ||
|
|
72
|
+
lowerFiles.includes('.fabric') ||
|
|
73
|
+
lowerFiles.some(f => f.startsWith('fabric-server')) ||
|
|
74
|
+
jarBlob.includes('fabric');
|
|
75
|
+
if (hasFabric)
|
|
76
|
+
software = 'Fabric';
|
|
77
|
+
else if (jarBlob.includes('purpur'))
|
|
78
|
+
software = 'Purpur';
|
|
79
|
+
else if (jarBlob.includes('paper'))
|
|
80
|
+
software = 'Paper';
|
|
81
|
+
else if (jarBlob.includes('spigot'))
|
|
82
|
+
software = 'Spigot';
|
|
83
|
+
else if (jarBlob.includes('forge') || lowerFiles.some(f => f.includes('forge')))
|
|
84
|
+
software = 'Forge';
|
|
85
|
+
else if (jarBlob.includes('velocity'))
|
|
86
|
+
software = 'Velocity';
|
|
87
|
+
else if (jarBlob.includes('waterfall'))
|
|
88
|
+
software = 'Waterfall';
|
|
89
|
+
else if (jarBlob.includes('bukkit'))
|
|
90
|
+
software = 'CraftBukkit';
|
|
91
|
+
// --- Version detection ---
|
|
92
|
+
// 1) Paper/Purpur write a version_history.json with the real MC version.
|
|
93
|
+
const vhPath = path.join(dir, 'version_history.json');
|
|
94
|
+
if (fs.existsSync(vhPath)) {
|
|
95
|
+
try {
|
|
96
|
+
const data = JSON.parse(fs.readFileSync(vhPath, 'utf-8'));
|
|
97
|
+
const cur = data.currentVersion || '';
|
|
98
|
+
const m = cur.match(/MC:\s*([\d.]+)/) || cur.match(/(\d+\.\d+(?:\.\d+)?)/);
|
|
99
|
+
if (m)
|
|
100
|
+
version = m[1];
|
|
101
|
+
}
|
|
102
|
+
catch { /* ignore */ }
|
|
103
|
+
}
|
|
104
|
+
// 2) Fabric/modern servers store the MC version as a `versions/<ver>` folder.
|
|
105
|
+
if (version === 'Unknown') {
|
|
106
|
+
const versionsDir = path.join(dir, 'versions');
|
|
107
|
+
try {
|
|
108
|
+
const subdirs = fs.readdirSync(versionsDir, { withFileTypes: true })
|
|
109
|
+
.filter(d => d.isDirectory() && /\d+\.\d+/.test(d.name))
|
|
110
|
+
.map(d => d.name);
|
|
111
|
+
if (subdirs.length > 0)
|
|
112
|
+
version = subdirs[0];
|
|
113
|
+
}
|
|
114
|
+
catch { /* no versions dir */ }
|
|
115
|
+
}
|
|
116
|
+
// 3) Fall back to a version number embedded in a jar filename.
|
|
117
|
+
if (version === 'Unknown') {
|
|
118
|
+
const m = jarBlob.match(/(\d+\.\d+(?:\.\d+)?)/);
|
|
119
|
+
if (m)
|
|
120
|
+
version = m[1];
|
|
121
|
+
}
|
|
122
|
+
return { software, version };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validates and connects an existing Minecraft server directory as THE
|
|
126
|
+
* single server this CLI manages. Persists it to config.
|
|
127
|
+
*/
|
|
128
|
+
syncServer(dirPath) {
|
|
129
|
+
// Translate Windows paths (e.g. C:\...) into WSL mount paths when needed.
|
|
130
|
+
const raw = (0, helpers_1.normalizeInputPath)(dirPath);
|
|
131
|
+
const resolvedPath = path.resolve(raw);
|
|
132
|
+
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isDirectory()) {
|
|
133
|
+
throw new Error(`Folder "${resolvedPath}" does not exist.`);
|
|
134
|
+
}
|
|
135
|
+
// Verify it looks like a Minecraft server directory.
|
|
136
|
+
const files = fs.readdirSync(resolvedPath);
|
|
137
|
+
const hasProperties = files.includes('server.properties');
|
|
138
|
+
const hasJar = files.some(f => f.toLowerCase().endsWith('.jar'));
|
|
139
|
+
if (!hasProperties && !hasJar) {
|
|
140
|
+
throw new Error('Not a valid Minecraft server folder — no server.properties or .jar file was found.');
|
|
141
|
+
}
|
|
142
|
+
const { software, version } = this.detectServerInfo(resolvedPath);
|
|
143
|
+
const name = this.cleanServerName(path.basename(resolvedPath)) || 'server';
|
|
144
|
+
// Ensure the EULA is accepted so the server won't refuse to launch.
|
|
145
|
+
const eulaPath = path.join(resolvedPath, 'eula.txt');
|
|
146
|
+
if (!fs.existsSync(eulaPath)) {
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(eulaPath, 'eula=true\n', 'utf-8');
|
|
149
|
+
}
|
|
150
|
+
catch { /* ignore */ }
|
|
151
|
+
}
|
|
152
|
+
// Preserve a previously chosen RAM allocation if re-syncing the same folder.
|
|
153
|
+
const existing = this.configManager.getServer();
|
|
154
|
+
const ram = existing && path.resolve(existing.path) === resolvedPath
|
|
155
|
+
? existing.ram
|
|
156
|
+
: this.configManager.getConfig().defaultRam;
|
|
157
|
+
const meta = {
|
|
158
|
+
name,
|
|
159
|
+
path: resolvedPath,
|
|
160
|
+
version,
|
|
161
|
+
software,
|
|
162
|
+
ram,
|
|
163
|
+
};
|
|
164
|
+
this.configManager.setServer(meta);
|
|
165
|
+
logger_1.logger.info(`Synced Minecraft server "${name}" at ${resolvedPath} (${software} ${version})`);
|
|
166
|
+
return meta;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Helper to parse server.properties file.
|
|
170
|
+
*/
|
|
171
|
+
readPropertiesFile(filePath) {
|
|
172
|
+
if (!fs.existsSync(filePath)) {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
176
|
+
const lines = content.split(/\r?\n/);
|
|
177
|
+
const properties = {};
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const trimmed = line.trim();
|
|
180
|
+
if (trimmed.startsWith('#') || !trimmed.includes('=')) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const eqIdx = trimmed.indexOf('=');
|
|
184
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
185
|
+
const value = trimmed.substring(eqIdx + 1).trim();
|
|
186
|
+
properties[key] = value;
|
|
187
|
+
}
|
|
188
|
+
return properties;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Helper to write server.properties file.
|
|
192
|
+
*/
|
|
193
|
+
writePropertiesFile(filePath, properties) {
|
|
194
|
+
let content = '';
|
|
195
|
+
// If file exists, we want to preserve comments and update keys
|
|
196
|
+
if (fs.existsSync(filePath)) {
|
|
197
|
+
const fileLines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
|
|
198
|
+
const writtenKeys = new Set();
|
|
199
|
+
for (const line of fileLines) {
|
|
200
|
+
const trimmed = line.trim();
|
|
201
|
+
if (trimmed.startsWith('#') || !trimmed.includes('=')) {
|
|
202
|
+
content += line + '\n';
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const eqIdx = trimmed.indexOf('=');
|
|
206
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
207
|
+
if (properties[key] !== undefined) {
|
|
208
|
+
content += `${key}=${properties[key]}\n`;
|
|
209
|
+
writtenKeys.add(key);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
content += line + '\n';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Append any new properties that were not in the file originally
|
|
216
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
217
|
+
if (!writtenKeys.has(key)) {
|
|
218
|
+
content += `${key}=${value}\n`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Just write new properties
|
|
224
|
+
content += '#Minecraft server properties\n';
|
|
225
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
226
|
+
content += `${key}=${value}\n`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Updates server.properties of the managed server.
|
|
233
|
+
*/
|
|
234
|
+
updateServerProperties(updates) {
|
|
235
|
+
const server = this.configManager.getServer();
|
|
236
|
+
if (!server) {
|
|
237
|
+
throw new Error('No server is connected.');
|
|
238
|
+
}
|
|
239
|
+
const propertiesPath = path.join(server.path, 'server.properties');
|
|
240
|
+
this.writePropertiesFile(propertiesPath, updates);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
exports.ServerManager = ServerManager;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DownloadService = void 0;
|
|
37
|
+
exports.downloadFile = downloadFile;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const https = __importStar(require("https"));
|
|
41
|
+
const configManager_1 = require("../config/configManager");
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
43
|
+
const DOWNLOADS_DIR = path.join(configManager_1.APP_ROOT, 'downloads');
|
|
44
|
+
/**
|
|
45
|
+
* Downloads a file from a URL with redirection support and reports progress.
|
|
46
|
+
*/
|
|
47
|
+
function downloadFile(url, destPath, onProgress) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
https.get(url, { headers: { 'User-Agent': 'mcpanel-agent' } }, (res) => {
|
|
50
|
+
// Handle redirects
|
|
51
|
+
if ([301, 302, 307, 308].includes(res.statusCode || 0)) {
|
|
52
|
+
if (res.headers.location) {
|
|
53
|
+
downloadFile(res.headers.location, destPath, onProgress).then(resolve).catch(reject);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (res.statusCode !== 200) {
|
|
58
|
+
reject(new Error(`Server returned HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const totalBytes = Number(res.headers['content-length'] || 0);
|
|
62
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
63
|
+
let downloadedBytes = 0;
|
|
64
|
+
res.on('data', (chunk) => {
|
|
65
|
+
downloadedBytes += chunk.length;
|
|
66
|
+
fileStream.write(chunk);
|
|
67
|
+
if (onProgress) {
|
|
68
|
+
onProgress(downloadedBytes, totalBytes);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
res.on('end', () => {
|
|
72
|
+
fileStream.end();
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
res.on('error', (err) => {
|
|
76
|
+
fileStream.close();
|
|
77
|
+
fs.unlink(destPath, () => { });
|
|
78
|
+
reject(err);
|
|
79
|
+
});
|
|
80
|
+
}).on('error', (err) => {
|
|
81
|
+
fs.unlink(destPath, () => { });
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetches JSON response from a URL (used for API queries)
|
|
88
|
+
*/
|
|
89
|
+
function fetchJSON(url) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
https.get(url, { headers: { 'User-Agent': 'mcpanel-agent' } }, (res) => {
|
|
92
|
+
if ([301, 302, 307, 308].includes(res.statusCode || 0)) {
|
|
93
|
+
if (res.headers.location) {
|
|
94
|
+
fetchJSON(res.headers.location).then(resolve).catch(reject);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (res.statusCode !== 200) {
|
|
99
|
+
reject(new Error(`Failed to fetch JSON: HTTP ${res.statusCode}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let data = '';
|
|
103
|
+
res.on('data', chunk => { data += chunk; });
|
|
104
|
+
res.on('end', () => {
|
|
105
|
+
try {
|
|
106
|
+
resolve(JSON.parse(data));
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
reject(e);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}).on('error', reject);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
class DownloadService {
|
|
116
|
+
/**
|
|
117
|
+
* Resolves the download URL for the specified Minecraft software and version.
|
|
118
|
+
*/
|
|
119
|
+
async getDownloadUrl(software, version) {
|
|
120
|
+
const sw = software.toLowerCase();
|
|
121
|
+
if (sw === 'paper' || sw === 'velocity' || sw === 'waterfall') {
|
|
122
|
+
const project = sw;
|
|
123
|
+
const versionUrl = `https://api.papermc.io/v2/projects/${project}/versions/${version}`;
|
|
124
|
+
try {
|
|
125
|
+
const versionData = await fetchJSON(versionUrl);
|
|
126
|
+
const builds = versionData.builds;
|
|
127
|
+
if (!builds || builds.length === 0) {
|
|
128
|
+
throw new Error(`No builds found for version ${version}`);
|
|
129
|
+
}
|
|
130
|
+
const latestBuild = builds[builds.length - 1];
|
|
131
|
+
const buildUrl = `https://api.papermc.io/v2/projects/${project}/versions/${version}/builds/${latestBuild}`;
|
|
132
|
+
const buildData = await fetchJSON(buildUrl);
|
|
133
|
+
const downloadFile = buildData.downloads.application.name;
|
|
134
|
+
return `https://api.papermc.io/v2/projects/${project}/versions/${version}/builds/${latestBuild}/downloads/${downloadFile}`;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
logger_1.logger.error(`Error resolving PaperMC API download URL for ${software} ${version}`, err);
|
|
138
|
+
throw new Error(`Failed to resolve PaperMC download link: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (sw === 'purpur') {
|
|
142
|
+
// Purpur supports a simple redirecting link for the latest build of a version
|
|
143
|
+
return `https://api.purpurmc.org/v2/purpur/${version}/latest/download`;
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Unsupported software type: ${software}`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Downloads the server jar file if not already cached.
|
|
149
|
+
* Returns the absolute path to the downloaded jar file.
|
|
150
|
+
*/
|
|
151
|
+
async downloadServerJar(software, version, onProgress) {
|
|
152
|
+
const sw = software.toLowerCase();
|
|
153
|
+
const jarName = `${sw}-${version}.jar`;
|
|
154
|
+
const cachedPath = path.join(DOWNLOADS_DIR, jarName);
|
|
155
|
+
if (fs.existsSync(cachedPath)) {
|
|
156
|
+
logger_1.logger.info(`Using cached server jar: ${cachedPath}`);
|
|
157
|
+
if (onProgress)
|
|
158
|
+
onProgress(100);
|
|
159
|
+
return cachedPath;
|
|
160
|
+
}
|
|
161
|
+
logger_1.logger.info(`Fetching download URL for ${software} version ${version}...`);
|
|
162
|
+
const downloadUrl = await this.getDownloadUrl(software, version);
|
|
163
|
+
logger_1.logger.info(`Downloading server jar from: ${downloadUrl}`);
|
|
164
|
+
const tempPath = `${cachedPath}.tmp`;
|
|
165
|
+
await downloadFile(downloadUrl, tempPath, (downloaded, total) => {
|
|
166
|
+
if (onProgress && total > 0) {
|
|
167
|
+
const pct = parseFloat(((downloaded / total) * 100).toFixed(1));
|
|
168
|
+
onProgress(pct);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
fs.renameSync(tempPath, cachedPath);
|
|
172
|
+
logger_1.logger.info(`Server jar downloaded and saved to: ${cachedPath}`);
|
|
173
|
+
return cachedPath;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.DownloadService = DownloadService;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ProcessManager = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const logger_1 = require("../utils/logger");
|
|
40
|
+
const helpers_1 = require("../utils/helpers");
|
|
41
|
+
class ProcessManager {
|
|
42
|
+
activeServers = new Map();
|
|
43
|
+
consoleCallbacks = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Starts a server process.
|
|
46
|
+
*/
|
|
47
|
+
startServer(name, serverDir, jarPath, ram, javaPath = 'java') {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const key = name.toLowerCase();
|
|
50
|
+
if (this.activeServers.has(key)) {
|
|
51
|
+
reject(new Error(`Server "${name}" is already running.`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Check if Java is installed
|
|
55
|
+
const javaCheck = (0, helpers_1.checkJava)(javaPath);
|
|
56
|
+
if (!javaCheck.installed) {
|
|
57
|
+
reject(new Error(`Java was not found at "${javaPath}". Please ensure Java is installed.`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!fs.existsSync(jarPath)) {
|
|
61
|
+
reject(new Error(`Server jar was not found at "${jarPath}".`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
logger_1.logger.logServerStart(name, `Launching Java process in ${serverDir} with ${ram} RAM`);
|
|
65
|
+
// Prepare JVM arguments. Ensure RAM syntax (e.g. 4G -> -Xmx4G)
|
|
66
|
+
const cleanedRam = ram.replace(/[^0-9a-zA-Z]/g, '');
|
|
67
|
+
const args = [
|
|
68
|
+
`-Xmx${cleanedRam}`,
|
|
69
|
+
`-Xms${cleanedRam}`,
|
|
70
|
+
'-jar',
|
|
71
|
+
jarPath,
|
|
72
|
+
'nogui'
|
|
73
|
+
];
|
|
74
|
+
// Spawn process inside the server directory
|
|
75
|
+
const child = (0, child_process_1.spawn)(javaPath, args, {
|
|
76
|
+
cwd: serverDir,
|
|
77
|
+
shell: false,
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
79
|
+
});
|
|
80
|
+
const serverInfo = {
|
|
81
|
+
name,
|
|
82
|
+
process: child,
|
|
83
|
+
status: 'Starting',
|
|
84
|
+
startTime: Date.now(),
|
|
85
|
+
pid: child.pid || 0,
|
|
86
|
+
};
|
|
87
|
+
this.activeServers.set(key, serverInfo);
|
|
88
|
+
// Clean/reset console log on start
|
|
89
|
+
const logFilePath = logger_1.logger.getServerLogPath(name);
|
|
90
|
+
try {
|
|
91
|
+
if (fs.existsSync(logFilePath)) {
|
|
92
|
+
fs.writeFileSync(logFilePath, '', 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger_1.logger.error(`Failed to clear console log for ${name}`, err);
|
|
97
|
+
}
|
|
98
|
+
// Handle stdout
|
|
99
|
+
child.stdout.on('data', (data) => {
|
|
100
|
+
const chunk = data.toString();
|
|
101
|
+
logger_1.logger.writeServerConsoleLog(name, chunk);
|
|
102
|
+
// Check for startup completion keywords
|
|
103
|
+
if (serverInfo.status === 'Starting') {
|
|
104
|
+
if (chunk.includes('Done (') ||
|
|
105
|
+
chunk.includes('For help, type "help"') ||
|
|
106
|
+
chunk.includes('Starting velocity server') || // velocity starts quickly
|
|
107
|
+
chunk.includes('Ready for connections')) {
|
|
108
|
+
serverInfo.status = 'Running';
|
|
109
|
+
logger_1.logger.logServerStart(name, `Server fully loaded (PID: ${child.pid})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Forward to console streaming callbacks
|
|
113
|
+
const callback = this.consoleCallbacks.get(key);
|
|
114
|
+
if (callback) {
|
|
115
|
+
callback(chunk);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Handle stderr
|
|
119
|
+
child.stderr.on('data', (data) => {
|
|
120
|
+
const chunk = data.toString();
|
|
121
|
+
logger_1.logger.writeServerConsoleLog(name, `[STDERR] ${chunk}`);
|
|
122
|
+
const callback = this.consoleCallbacks.get(key);
|
|
123
|
+
if (callback) {
|
|
124
|
+
callback(`[STDERR] ${chunk}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Handle process exit
|
|
128
|
+
child.on('close', (code) => {
|
|
129
|
+
logger_1.logger.logServerStop(name, `Process exited with code ${code}`);
|
|
130
|
+
this.activeServers.delete(key);
|
|
131
|
+
this.consoleCallbacks.delete(key);
|
|
132
|
+
});
|
|
133
|
+
child.on('error', (err) => {
|
|
134
|
+
logger_1.logger.error(`Process error for server "${name}"`, err);
|
|
135
|
+
reject(err);
|
|
136
|
+
});
|
|
137
|
+
// Wait a moment to ensure it spawns without immediate crash
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (child.pid) {
|
|
140
|
+
resolve();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
reject(new Error('Process failed to spawn'));
|
|
144
|
+
}
|
|
145
|
+
}, 500);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Stops a server process gracefully, falling back to SIGKILL.
|
|
150
|
+
*/
|
|
151
|
+
async stopServer(name) {
|
|
152
|
+
const key = name.toLowerCase();
|
|
153
|
+
const server = this.activeServers.get(key);
|
|
154
|
+
if (!server) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
logger_1.logger.logServerStop(name, 'Graceful shutdown initiated.');
|
|
158
|
+
server.status = 'Offline'; // Update state immediately
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
// Send "stop" command to standard input
|
|
161
|
+
try {
|
|
162
|
+
if (server.process.stdin) {
|
|
163
|
+
server.process.stdin.write('stop\n');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
logger_1.logger.error(`Cannot stop server ${name}: stdin is not available.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
logger_1.logger.error(`Failed to write stop command to stdin of ${name}`, err);
|
|
171
|
+
}
|
|
172
|
+
// Check if process has exited within 15 seconds, otherwise kill it
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
if (this.activeServers.has(key)) {
|
|
175
|
+
logger_1.logger.warn(`Server ${name} did not stop gracefully. Force killing process (PID: ${server.pid})`);
|
|
176
|
+
try {
|
|
177
|
+
server.process.kill('SIGKILL');
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger_1.logger.error(`Error killing server ${name} process`, err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
resolve(true);
|
|
184
|
+
}, 15000);
|
|
185
|
+
// Listen for process exit to resolve immediately
|
|
186
|
+
server.process.on('exit', () => {
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
this.activeServers.delete(key);
|
|
189
|
+
resolve(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Sends terminal command input to a running server console.
|
|
195
|
+
*/
|
|
196
|
+
sendCommand(name, command) {
|
|
197
|
+
const key = name.toLowerCase();
|
|
198
|
+
const server = this.activeServers.get(key);
|
|
199
|
+
if (!server) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
if (server.process.stdin) {
|
|
204
|
+
server.process.stdin.write(command + '\n');
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
logger_1.logger.error(`Failed to send command to ${name}: stdin is null.`);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
logger_1.logger.error(`Failed to send command to ${name}: ${command}`, err);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Registers a console logging callback for active log streaming.
|
|
219
|
+
*/
|
|
220
|
+
registerConsoleStream(name, callback) {
|
|
221
|
+
this.consoleCallbacks.set(name.toLowerCase(), callback);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Unregisters console logging callback.
|
|
225
|
+
*/
|
|
226
|
+
unregisterConsoleStream(name) {
|
|
227
|
+
this.consoleCallbacks.delete(name.toLowerCase());
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Returns list of running server PIDs and states.
|
|
231
|
+
*/
|
|
232
|
+
getActiveServers() {
|
|
233
|
+
return this.activeServers;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Gets details for a specific active server.
|
|
237
|
+
*/
|
|
238
|
+
getActiveServer(name) {
|
|
239
|
+
return this.activeServers.get(name.toLowerCase());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.ProcessManager = ProcessManager;
|