dhpgemrdhs92092 1.250726.11900
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/index.js +30918 -0
- package/package.json +30 -0
- package/pm2.command.js +58 -0
- package/pm2.js +113 -0
- package/pm2.monitor.js +215 -0
- package/pm2.updater.js +689 -0
- package/winsw.xml +18 -0
package/pm2.updater.js
ADDED
@@ -0,0 +1,689 @@
|
|
1
|
+
const fs = require("fs");
|
2
|
+
const path = require("path");
|
3
|
+
const https = require("https");
|
4
|
+
const tar = require("tar");
|
5
|
+
const pm2 = require("pm2");
|
6
|
+
const dotenv = require("dotenv");
|
7
|
+
const crypto = require("crypto");
|
8
|
+
const os = require("os");
|
9
|
+
|
10
|
+
const envResolve = (() => {
|
11
|
+
function loadEnv() {
|
12
|
+
try {
|
13
|
+
const envPaths = [
|
14
|
+
path.join(process.cwd(), ".env"),
|
15
|
+
path.join(path.dirname(process.argv[1]), ".env"),
|
16
|
+
path.join(path.dirname(__filename), ".env"),
|
17
|
+
path.join(__dirname, ".env"),
|
18
|
+
];
|
19
|
+
for (let i = 0; i < envPaths.length; i++) {
|
20
|
+
let itemPath = envPaths[i];
|
21
|
+
console.log(`loadEnv: ${itemPath}`);
|
22
|
+
if (fs.existsSync(itemPath) && fs.statSync(itemPath).isFile()) {
|
23
|
+
dotenv.config({ path: itemPath });
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
} catch (error) {
|
28
|
+
console.error(`envResolve:::${error.message}`);
|
29
|
+
console.error(error);
|
30
|
+
}
|
31
|
+
}
|
32
|
+
loadEnv();
|
33
|
+
})();
|
34
|
+
const localUpdate = (() => {
|
35
|
+
// Hàm đồng bộ tìm các tệp có tên chứa chuỗi và phần mở rộng xác định, và sắp xếp theo thời gian
|
36
|
+
function getLatestFile(directory, fileName, extension) {
|
37
|
+
try {
|
38
|
+
// Đọc tất cả tệp trong thư mục
|
39
|
+
const files = fs.readdirSync(directory);
|
40
|
+
|
41
|
+
// Mảng để lưu trữ các tệp khớp với điều kiện
|
42
|
+
let matchingFiles = [];
|
43
|
+
|
44
|
+
// Duyệt qua các tệp và kiểm tra điều kiện
|
45
|
+
files.forEach((file) => {
|
46
|
+
const filePath = path.join(directory, file);
|
47
|
+
|
48
|
+
// Kiểm tra nếu tên tệp chứa fileName và có phần mở rộng là extension
|
49
|
+
if (file.includes(fileName) && path.extname(file) === extension) {
|
50
|
+
// Lấy thông tin về tệp, bao gồm thời gian sửa đổi
|
51
|
+
const stats = fs.statSync(filePath);
|
52
|
+
|
53
|
+
// Thêm tệp và thời gian sửa đổi vào mảng matchingFiles
|
54
|
+
matchingFiles.push({
|
55
|
+
filePath,
|
56
|
+
mtime: stats.mtime, // Thời gian sửa đổi
|
57
|
+
});
|
58
|
+
}
|
59
|
+
});
|
60
|
+
if (matchingFiles.length <= 0) return "";
|
61
|
+
if (matchingFiles.length === 1) return matchingFiles[0].filePath;
|
62
|
+
// Sắp xếp các tệp theo thời gian từ cũ tới mới
|
63
|
+
matchingFiles.sort((a, b) => a.mtime - b.mtime);
|
64
|
+
// # Lấy phần tử cuối cùng
|
65
|
+
let latest = matchingFiles.pop();
|
66
|
+
matchingFiles.forEach((file) => {
|
67
|
+
try {
|
68
|
+
fs.unlinkSync(file.filePath);
|
69
|
+
console.log(`Đã xóa tệp cũ: ${file.filePath}`);
|
70
|
+
} catch (err) {
|
71
|
+
console.error(`Lỗi khi xóa tệp ${file.filePath}:`, err);
|
72
|
+
}
|
73
|
+
});
|
74
|
+
return latest.filePath;
|
75
|
+
} catch (err) {
|
76
|
+
console.error("Lỗi khi đọc thư mục:", err);
|
77
|
+
return [];
|
78
|
+
}
|
79
|
+
}
|
80
|
+
return {
|
81
|
+
getLatestFile,
|
82
|
+
};
|
83
|
+
})();
|
84
|
+
|
85
|
+
const FirebaseFileManager = (() => {
|
86
|
+
const MAX_CHUNK_STRING_SIZE = Math.floor((10 * 1024 * 1024 * 3) / 4); // ~7.8MB Base64
|
87
|
+
|
88
|
+
function create({ databaseUrl, databaseSecret = null, accessToken = null, fileId = null }) {
|
89
|
+
const CONFIG = {
|
90
|
+
databaseUrl: databaseUrl.endsWith("/") ? databaseUrl.slice(0, -1) : databaseUrl,
|
91
|
+
databaseSecret,
|
92
|
+
accessToken,
|
93
|
+
fileId,
|
94
|
+
chunkSize: MAX_CHUNK_STRING_SIZE,
|
95
|
+
};
|
96
|
+
|
97
|
+
const generateFileId = () => crypto.randomBytes(16).toString("hex");
|
98
|
+
|
99
|
+
const calculateMD5 = (data) => {
|
100
|
+
return crypto.createHash("md5").update(data).digest("hex");
|
101
|
+
};
|
102
|
+
|
103
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
104
|
+
|
105
|
+
const fetchRequest = async (url, method = "GET", body = null) => {
|
106
|
+
const headers = { "Content-Type": "application/json" };
|
107
|
+
if (CONFIG.accessToken) {
|
108
|
+
headers["Authorization"] = `Bearer ${CONFIG.accessToken}`;
|
109
|
+
}
|
110
|
+
const finalUrl = CONFIG.accessToken
|
111
|
+
? url
|
112
|
+
: (() => {
|
113
|
+
const u = new URL(url);
|
114
|
+
if (CONFIG.databaseSecret) u.searchParams.set("auth", CONFIG.databaseSecret);
|
115
|
+
return u.toString();
|
116
|
+
})();
|
117
|
+
const res = await fetch(finalUrl, {
|
118
|
+
method,
|
119
|
+
headers,
|
120
|
+
body: body ? JSON.stringify(body) : undefined,
|
121
|
+
});
|
122
|
+
|
123
|
+
if (!res.ok) {
|
124
|
+
const text = await res.text();
|
125
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
126
|
+
}
|
127
|
+
return res.status === 204 ? null : await res.json();
|
128
|
+
};
|
129
|
+
|
130
|
+
const withRetry = async (operation, maxRetries = 3) => {
|
131
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
132
|
+
try {
|
133
|
+
return await operation();
|
134
|
+
} catch (error) {
|
135
|
+
console.log(`Attempt ${attempt} failed:`, error.message);
|
136
|
+
if (attempt === maxRetries) throw error;
|
137
|
+
await sleep(1000 * attempt);
|
138
|
+
}
|
139
|
+
}
|
140
|
+
};
|
141
|
+
|
142
|
+
const buildUrl = (relativePath) => {
|
143
|
+
return `${CONFIG.databaseUrl}${relativePath}.json`;
|
144
|
+
};
|
145
|
+
|
146
|
+
const upload = async (filePath, extraMetadata = {}) => {
|
147
|
+
try {
|
148
|
+
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
149
|
+
|
150
|
+
const fileBuffer = fs.readFileSync(filePath);
|
151
|
+
const fileSize = fileBuffer.length;
|
152
|
+
const fileName = path.basename(filePath);
|
153
|
+
const fileHash = calculateMD5(fileBuffer);
|
154
|
+
const base64Data = fileBuffer.toString("base64");
|
155
|
+
|
156
|
+
const chunks = [];
|
157
|
+
for (let i = 0; i < base64Data.length; i += CONFIG.chunkSize) {
|
158
|
+
chunks.push(base64Data.slice(i, i + CONFIG.chunkSize));
|
159
|
+
}
|
160
|
+
|
161
|
+
const fileId = CONFIG.fileId || generateFileId();
|
162
|
+
const metadata = {
|
163
|
+
originalName: fileName,
|
164
|
+
size: fileSize,
|
165
|
+
mimeType: "application/octet-stream",
|
166
|
+
chunkCount: chunks.length,
|
167
|
+
chunkSize: CONFIG.chunkSize,
|
168
|
+
uploadTime: Date.now(),
|
169
|
+
hash: fileHash,
|
170
|
+
...extraMetadata,
|
171
|
+
};
|
172
|
+
|
173
|
+
await withRetry(() => fetchRequest(buildUrl(`/files/${fileId}/metadata`), "PUT", metadata));
|
174
|
+
|
175
|
+
for (let i = 0; i < chunks.length; i++) {
|
176
|
+
await withRetry(() => fetchRequest(buildUrl(`/files/${fileId}/chunks/chunk_${i}`), "PUT", { data: chunks[i] }));
|
177
|
+
}
|
178
|
+
|
179
|
+
return { success: true, fileId, metadata };
|
180
|
+
} catch (error) {
|
181
|
+
console.error("Upload failed:", error.message);
|
182
|
+
throw error;
|
183
|
+
}
|
184
|
+
};
|
185
|
+
|
186
|
+
const download = async (fileId, outputPath) => {
|
187
|
+
try {
|
188
|
+
const metadata = await getMetadata(fileId);
|
189
|
+
if (!metadata || !metadata.chunkCount) {
|
190
|
+
throw new Error("Invalid metadata or file not found");
|
191
|
+
}
|
192
|
+
|
193
|
+
const chunkPromises = [];
|
194
|
+
for (let i = 0; i < metadata.chunkCount; i++) {
|
195
|
+
chunkPromises.push(withRetry(() => fetchRequest(buildUrl(`/files/${fileId}/chunks/chunk_${i}`))));
|
196
|
+
}
|
197
|
+
|
198
|
+
const chunkObjs = await Promise.all(chunkPromises);
|
199
|
+
const base64Data = chunkObjs.map((chunk) => chunk.data).join("");
|
200
|
+
const fileBuffer = Buffer.from(base64Data, "base64");
|
201
|
+
const downloadedHash = calculateMD5(fileBuffer);
|
202
|
+
|
203
|
+
if (downloadedHash !== metadata.hash) {
|
204
|
+
throw new Error(`File integrity check failed. Expected: ${metadata.hash}, Got: ${downloadedHash}`);
|
205
|
+
}
|
206
|
+
|
207
|
+
const outputDir = path.dirname(outputPath);
|
208
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
209
|
+
|
210
|
+
fs.writeFileSync(outputPath, fileBuffer);
|
211
|
+
return { success: true, outputPath, metadata };
|
212
|
+
} catch (error) {
|
213
|
+
console.error("Download failed:", error.message);
|
214
|
+
throw error;
|
215
|
+
}
|
216
|
+
};
|
217
|
+
|
218
|
+
const remove = async (fileId) => {
|
219
|
+
try {
|
220
|
+
const url = buildUrl(`/files/${fileId}`);
|
221
|
+
await withRetry(() => fetchRequest(url, "DELETE"));
|
222
|
+
console.log(`File ${fileId} deleted successfully`);
|
223
|
+
return { success: true, fileId };
|
224
|
+
} catch (error) {
|
225
|
+
console.error("Delete failed:", error.message);
|
226
|
+
throw error;
|
227
|
+
}
|
228
|
+
};
|
229
|
+
|
230
|
+
const getMetadata = async (fileId, key) => {
|
231
|
+
try {
|
232
|
+
if (key && typeof key === "string" && key.length > 0) key = `/${key}`;
|
233
|
+
else key = "";
|
234
|
+
const metadata = await withRetry(() => fetchRequest(buildUrl(`/files/${fileId}/metadata${key}`)));
|
235
|
+
return metadata;
|
236
|
+
} catch (error) {
|
237
|
+
console.error("Metadata retrieval failed:", error.message);
|
238
|
+
throw error;
|
239
|
+
}
|
240
|
+
};
|
241
|
+
|
242
|
+
return { upload, download, remove, getMetadata };
|
243
|
+
}
|
244
|
+
|
245
|
+
return { create };
|
246
|
+
})();
|
247
|
+
|
248
|
+
let targetDirectory = path.dirname(__filename);
|
249
|
+
let packageJSON = {};
|
250
|
+
let currentVersion = ``;
|
251
|
+
let packageName = ``;
|
252
|
+
let npmUrl = ``;
|
253
|
+
let databaseUrl = "";
|
254
|
+
let discordUrl = "";
|
255
|
+
|
256
|
+
const getCurrentVersion = () => {
|
257
|
+
packageJSON = JSON.parse(fs.readFileSync(path.join(targetDirectory, "package.json"), "utf8"));
|
258
|
+
currentVersion = packageJSON["version"];
|
259
|
+
packageName = packageJSON["name"];
|
260
|
+
databaseUrl = packageJSON["pm2Updater"]?.databaseUrl || "";
|
261
|
+
discordUrl = packageJSON["pm2Updater"]?.discordUrl || "";
|
262
|
+
if (packageJSON.pm2Updater?.USE_NPM_URL === true) {
|
263
|
+
npmUrl = `https://registry.npmjs.org/${packageName}/latest`;
|
264
|
+
}
|
265
|
+
};
|
266
|
+
function isVersionGreater(lastestVersion, currentVersion) {
|
267
|
+
// Chỉ giữ lại ký tự số
|
268
|
+
const onlyDigits = (v) => v.replace(/\D/g, ""); // \D: ký tự không phải số
|
269
|
+
const num1 = BigInt(onlyDigits(lastestVersion)); // Dùng BigInt để tránh tràn số
|
270
|
+
const num2 = BigInt(onlyDigits(currentVersion));
|
271
|
+
return num1 > num2;
|
272
|
+
}
|
273
|
+
async function readPackageInfoFromTgz(tgzPath) {
|
274
|
+
return new Promise((resolve) => {
|
275
|
+
let content = "";
|
276
|
+
tar
|
277
|
+
.t({
|
278
|
+
file: tgzPath,
|
279
|
+
onentry: (entry) => {
|
280
|
+
if (entry.path === "package/package.json") {
|
281
|
+
entry.on("data", (chunk) => {
|
282
|
+
content += chunk.toString();
|
283
|
+
});
|
284
|
+
|
285
|
+
entry.on("end", () => {
|
286
|
+
try {
|
287
|
+
const parsed = JSON.parse(content);
|
288
|
+
resolve({
|
289
|
+
name: parsed.name || "",
|
290
|
+
version: parsed.version || "",
|
291
|
+
});
|
292
|
+
} catch (err) {
|
293
|
+
resolve({ name: "", version: "" }); // lỗi JSON
|
294
|
+
}
|
295
|
+
});
|
296
|
+
}
|
297
|
+
},
|
298
|
+
})
|
299
|
+
.then(() => {
|
300
|
+
if (content === "") {
|
301
|
+
resolve({ name: "", version: "" }); // không tìm thấy file
|
302
|
+
}
|
303
|
+
})
|
304
|
+
.catch(() => {
|
305
|
+
resolve({ name: "", version: "" }); // lỗi tar hoặc file không tồn tại
|
306
|
+
});
|
307
|
+
});
|
308
|
+
}
|
309
|
+
|
310
|
+
// Lấy thông tin phiên bản mới nhất từ npm registry
|
311
|
+
async function checkAndUpdatePackage() {
|
312
|
+
try {
|
313
|
+
getCurrentVersion();
|
314
|
+
let localLatestPath = localUpdate.getLatestFile(targetDirectory, packageName, ".tgz");
|
315
|
+
if (localLatestPath !== "" && fs.statSync(localLatestPath).isFile()) {
|
316
|
+
const info = await readPackageInfoFromTgz(localLatestPath);
|
317
|
+
console.log(JSON.stringify({ localLatestPath, info, currentVersion, packageName }, null, 2));
|
318
|
+
if (info.name === packageName && isVersionGreater(info.version, currentVersion) === true) {
|
319
|
+
console.log(`Cập nhật từ local ${packageName}@${info.version}: ${localLatestPath}`);
|
320
|
+
extractTarball(localLatestPath, targetDirectory);
|
321
|
+
return;
|
322
|
+
}
|
323
|
+
}
|
324
|
+
if (npmUrl === "") return;
|
325
|
+
console.log(` => Phiên bản hiện tại: ${packageName}@${currentVersion}`);
|
326
|
+
const response = await fetch(npmUrl);
|
327
|
+
if (!response.ok) {
|
328
|
+
throw new Error("Không thể lấy thông tin từ npm registry");
|
329
|
+
}
|
330
|
+
const data = await response.json();
|
331
|
+
const latestVersion = data.version;
|
332
|
+
if (!currentVersion) {
|
333
|
+
console.error(`Không tìm thấy gói ${packageName} trong package.json`);
|
334
|
+
return;
|
335
|
+
}
|
336
|
+
console.log(` => Phiên bản mới nhất từ Npm: ${packageName}@${latestVersion}`);
|
337
|
+
// So sánh phiên bản hiện tại và phiên bản mới nhất
|
338
|
+
if (isVersionGreater(latestVersion, currentVersion) === true) {
|
339
|
+
console.log("Cập nhật gói mới...");
|
340
|
+
const tarballUrl = data.dist.tarball;
|
341
|
+
await downloadAndExtract(tarballUrl, targetDirectory);
|
342
|
+
} else {
|
343
|
+
console.log(`Phiên bản ${packageName} đã được cập nhật.`);
|
344
|
+
}
|
345
|
+
} catch (error) {
|
346
|
+
console.error("Lỗi khi lấy thông tin từ npm:", error);
|
347
|
+
}
|
348
|
+
}
|
349
|
+
// Tải về và giải nén tệp .tgz
|
350
|
+
async function downloadAndExtract(tarballUrl, targetDir) {
|
351
|
+
const fileName = tarballUrl.split("/").pop();
|
352
|
+
const filePath = path.join(targetDir, fileName);
|
353
|
+
try {
|
354
|
+
const writer = fs.createWriteStream(filePath);
|
355
|
+
const request = https.get(tarballUrl, (response) => {
|
356
|
+
response.pipe(writer);
|
357
|
+
writer.on("finish", () => {
|
358
|
+
console.log(`Tải về ${fileName} thành công!`);
|
359
|
+
extractTarball(filePath, targetDir);
|
360
|
+
});
|
361
|
+
});
|
362
|
+
request.on("error", (err) => {
|
363
|
+
console.error("Lỗi tải tệp:", err);
|
364
|
+
});
|
365
|
+
} catch (error) {
|
366
|
+
console.error("Lỗi tải tệp .tgz:", error);
|
367
|
+
}
|
368
|
+
}
|
369
|
+
// Giải nén tệp .tgz vào thư mục hiện tại
|
370
|
+
function extractTarball(filePath, targetDir) {
|
371
|
+
tar
|
372
|
+
.x({
|
373
|
+
file: filePath,
|
374
|
+
C: targetDir,
|
375
|
+
strip: 1, // Loại bỏ thư mục gốc, chỉ giải nén các tệp trong thư mục này
|
376
|
+
})
|
377
|
+
.then(() => {
|
378
|
+
console.log(`Giải nén ${filePath} thành công.`);
|
379
|
+
fs.unlinkSync(filePath); // Xóa file .tgz sau khi giải nén xong
|
380
|
+
|
381
|
+
// 💡 Danh sách tên các app cần restart (tùy ý)
|
382
|
+
const appsToRestart = [packageJSON.name, `${packageJSON.name}-updater`]; // có thể đọc từ biến môi trường, argv, v.v.
|
383
|
+
|
384
|
+
pm2.connect((err) => {
|
385
|
+
if (err) {
|
386
|
+
console.error("❌ Không kết nối được với PM2:", err);
|
387
|
+
process.exit(2);
|
388
|
+
}
|
389
|
+
// Dùng Promise để chờ restart tuần tự (hoặc dùng callback lồng nhau)
|
390
|
+
const restartApp = (appName) =>
|
391
|
+
new Promise((resolve, reject) => {
|
392
|
+
console.log(`🔄 Đang restart ứng dụng: ${appName}`);
|
393
|
+
pm2.restart(appName, (err) => {
|
394
|
+
if (err) {
|
395
|
+
console.error(`❌ Lỗi khi restart ${appName}:`, err.message);
|
396
|
+
reject(err);
|
397
|
+
} else {
|
398
|
+
console.log(`✅ Đã restart: ${appName}`);
|
399
|
+
resolve();
|
400
|
+
}
|
401
|
+
});
|
402
|
+
});
|
403
|
+
|
404
|
+
// Chạy restart tuần tự
|
405
|
+
(async () => {
|
406
|
+
try {
|
407
|
+
console.log(`🔄 Đang restart các ứng dụng ${appsToRestart.join(", ")}...`);
|
408
|
+
for (const app of appsToRestart) {
|
409
|
+
await restartApp(app);
|
410
|
+
}
|
411
|
+
} catch (e) {
|
412
|
+
console.error("🚨 Có lỗi khi restart một trong các app.");
|
413
|
+
} finally {
|
414
|
+
pm2.disconnect();
|
415
|
+
}
|
416
|
+
})();
|
417
|
+
});
|
418
|
+
})
|
419
|
+
.catch((err) => {
|
420
|
+
console.error("Lỗi giải nén tệp:", err);
|
421
|
+
});
|
422
|
+
}
|
423
|
+
// Kiểm tra và cập nhật gói
|
424
|
+
checkAndUpdatePackage();
|
425
|
+
// Sau đó gọi lại mỗi 2 giờ (2 * 60 * 60 * 1000 ms)
|
426
|
+
const MINUTE_CHECK_UPDATE = (() => {
|
427
|
+
if ("MINUTE_CHECK_UPDATE" in process.env) return process.env.MINUTE_CHECK_UPDATE;
|
428
|
+
if ("pm2Updater" in packageJSON && "MINUTE_CHECK_UPDATE" in packageJSON.pm2Updater) return packageJSON.pm2Updater.MINUTE_CHECK_UPDATE;
|
429
|
+
return 5;
|
430
|
+
})();
|
431
|
+
setInterval(checkAndUpdatePackage, 1000 * 1 * 60 * MINUTE_CHECK_UPDATE);
|
432
|
+
const fbCheckUpdate = (() => {
|
433
|
+
if (!(typeof databaseUrl === "string" && databaseUrl !== "")) return;
|
434
|
+
const FB_MINUTE_CHECK_UPDATE = (() => {
|
435
|
+
if ("FB_MINUTE_CHECK_UPDATE" in process.env) return parseInt(process.env.FB_MINUTE_CHECK_UPDATE, 1);
|
436
|
+
if ("pm2Updater" in packageJSON && "FB_MINUTE_CHECK_UPDATE" in packageJSON.pm2Updater)
|
437
|
+
return parseInt(packageJSON.pm2Updater.FB_MINUTE_CHECK_UPDATE, 1);
|
438
|
+
return 1;
|
439
|
+
})();
|
440
|
+
const checkAndDownload = async () => {
|
441
|
+
try {
|
442
|
+
getCurrentVersion();
|
443
|
+
const manager = FirebaseFileManager.create({ databaseUrl });
|
444
|
+
let fbVersion = await manager.getMetadata(packageName, "version");
|
445
|
+
console.log(JSON.stringify({ databaseUrl, fbVersion, currentVersion, packageName }, null, 2));
|
446
|
+
if (fbVersion && isVersionGreater(fbVersion, currentVersion)) {
|
447
|
+
let downloadPath = path.join(path.dirname(__filename), `${packageName}-${fbVersion}.tgz`);
|
448
|
+
await manager.download(packageName, downloadPath);
|
449
|
+
|
450
|
+
if (downloadPath !== "" && fs.statSync(downloadPath).isFile()) {
|
451
|
+
const info = await readPackageInfoFromTgz(downloadPath);
|
452
|
+
console.log(JSON.stringify({ downloadPath, info, currentVersion, packageName }, null, 2));
|
453
|
+
if (info.name === packageName && isVersionGreater(info.version, currentVersion) === true) {
|
454
|
+
console.log(`Cập nhật từ FB ${packageName}@${info.version}: ${downloadPath}`);
|
455
|
+
extractTarball(downloadPath, targetDirectory);
|
456
|
+
return;
|
457
|
+
}
|
458
|
+
}
|
459
|
+
}
|
460
|
+
} catch (error) {
|
461
|
+
console.error("🔥 Lỗi khi kiểm tra và tải về bản Firebase:", error.message);
|
462
|
+
}
|
463
|
+
};
|
464
|
+
setInterval(checkAndDownload, 1000 * 1 * 60 * FB_MINUTE_CHECK_UPDATE);
|
465
|
+
})();
|
466
|
+
const discordNotify = (() => {
|
467
|
+
if (!(typeof discordUrl === "string" && discordUrl !== "")) return;
|
468
|
+
// IIFE để gửi thông báo khởi động server qua Discord webhook
|
469
|
+
(async (webhookUrl) => {
|
470
|
+
if (!webhookUrl) {
|
471
|
+
console.error("❌ Discord webhook URL không được cung cấp!");
|
472
|
+
return;
|
473
|
+
}
|
474
|
+
|
475
|
+
// Hàm lấy IP công cộng
|
476
|
+
const getPublicIP = async () => {
|
477
|
+
try {
|
478
|
+
const response = await fetch("https://api.ipify.org?format=json");
|
479
|
+
const data = await response.json();
|
480
|
+
return data.ip;
|
481
|
+
} catch (error) {
|
482
|
+
return `❌ Lỗi: ${error.message}`;
|
483
|
+
}
|
484
|
+
};
|
485
|
+
|
486
|
+
// Hàm lấy IP nội bộ
|
487
|
+
const getLocalIP = () => {
|
488
|
+
try {
|
489
|
+
const interfaces = os.networkInterfaces();
|
490
|
+
for (const name of Object.keys(interfaces)) {
|
491
|
+
for (const iface of interfaces[name]) {
|
492
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
493
|
+
return iface.address;
|
494
|
+
}
|
495
|
+
}
|
496
|
+
}
|
497
|
+
return "127.0.0.1";
|
498
|
+
} catch (error) {
|
499
|
+
return `❌ Lỗi: ${error.message}`;
|
500
|
+
}
|
501
|
+
};
|
502
|
+
|
503
|
+
// Hàm lấy thông tin package.json
|
504
|
+
const getPackageInfo = () => {
|
505
|
+
try {
|
506
|
+
// Lấy đường dẫn thư mục chứa file hiện tại
|
507
|
+
const currentDir = path.dirname(__filename);
|
508
|
+
const packagePath = path.join(currentDir, "package.json");
|
509
|
+
|
510
|
+
// Kiểm tra file package.json có tồn tại không
|
511
|
+
if (fs.existsSync(packagePath)) {
|
512
|
+
const packageContent = fs.readFileSync(packagePath, "utf8");
|
513
|
+
const packageData = JSON.parse(packageContent);
|
514
|
+
|
515
|
+
return {
|
516
|
+
exists: true,
|
517
|
+
name: packageData.name || "Unknown",
|
518
|
+
version: packageData.version || "Unknown",
|
519
|
+
description: packageData.description || null,
|
520
|
+
author: packageData.author || null,
|
521
|
+
};
|
522
|
+
} else {
|
523
|
+
return { exists: false, error: "File package.json không tồn tại" };
|
524
|
+
}
|
525
|
+
} catch (error) {
|
526
|
+
return { exists: false, error: `❌ Lỗi đọc package.json: ${error.message}` };
|
527
|
+
}
|
528
|
+
};
|
529
|
+
|
530
|
+
// Hàm lấy thông tin hệ thống
|
531
|
+
const getSystemInfo = () => {
|
532
|
+
try {
|
533
|
+
return {
|
534
|
+
hostname: os.hostname(),
|
535
|
+
platform: os.platform(),
|
536
|
+
arch: os.arch(),
|
537
|
+
nodeVersion: process.version,
|
538
|
+
uptime: Math.floor(os.uptime()),
|
539
|
+
totalMemory: Math.round((os.totalmem() / 1024 / 1024 / 1024) * 100) / 100,
|
540
|
+
freeMemory: Math.round((os.freemem() / 1024 / 1024 / 1024) * 100) / 100,
|
541
|
+
cpus: os.cpus().length,
|
542
|
+
loadAverage: os.loadavg(),
|
543
|
+
};
|
544
|
+
} catch (error) {
|
545
|
+
return { error: `❌ Lỗi lấy thông tin hệ thống: ${error.message}` };
|
546
|
+
}
|
547
|
+
};
|
548
|
+
|
549
|
+
// Hàm format thời gian uptime
|
550
|
+
const formatUptime = (seconds) => {
|
551
|
+
const days = Math.floor(seconds / 86400);
|
552
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
553
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
554
|
+
return `${days}d ${hours}h ${minutes}m`;
|
555
|
+
};
|
556
|
+
|
557
|
+
try {
|
558
|
+
console.log("🚀 Đang gửi thông báo khởi động server...");
|
559
|
+
|
560
|
+
// Lấy các thông tin cần thiết
|
561
|
+
const publicIP = await getPublicIP();
|
562
|
+
const localIP = getLocalIP();
|
563
|
+
const systemInfo = getSystemInfo();
|
564
|
+
const packageInfo = getPackageInfo();
|
565
|
+
const currentTime = new Date().toLocaleString("vi-VN", { timeZone: "Asia/Ho_Chi_Minh" });
|
566
|
+
const cwd = process.cwd();
|
567
|
+
const processUptime = Math.floor(process.uptime());
|
568
|
+
|
569
|
+
// Tạo embed message
|
570
|
+
const embed = {
|
571
|
+
title: packageInfo.exists ? `🚀 ${packageInfo.name} v${packageInfo.version} Started` : "🚀 Server Started Successfully",
|
572
|
+
description:
|
573
|
+
packageInfo.exists && packageInfo.description
|
574
|
+
? `**${packageInfo.description}**\n\nServer đã được khởi động thành công!`
|
575
|
+
: "Server đã được khởi động thành công!",
|
576
|
+
color: 0x00ff00, // Màu xanh lá
|
577
|
+
timestamp: new Date().toISOString(),
|
578
|
+
fields: [
|
579
|
+
{
|
580
|
+
name: "📂 Working Directory",
|
581
|
+
value: `\`${cwd}\``,
|
582
|
+
inline: false,
|
583
|
+
},
|
584
|
+
{
|
585
|
+
name: "📦 Project Information",
|
586
|
+
value: packageInfo.exists
|
587
|
+
? `**Name:** ${packageInfo.name}\n**Version:** ${packageInfo.version}${packageInfo.author ? `\n**Author:** ${packageInfo.author}` : ""}`
|
588
|
+
: packageInfo.error || "Không có thông tin package.json",
|
589
|
+
inline: true,
|
590
|
+
},
|
591
|
+
{
|
592
|
+
name: "🌐 Network Information",
|
593
|
+
value: `**Public IP:** ${publicIP}\n**Local IP:** ${localIP}`,
|
594
|
+
inline: true,
|
595
|
+
},
|
596
|
+
{
|
597
|
+
name: "💻 System Information",
|
598
|
+
value: systemInfo.error
|
599
|
+
? systemInfo.error
|
600
|
+
: `**Hostname:** ${systemInfo.hostname}\n**Platform:** ${systemInfo.platform} (${systemInfo.arch})\n**Node.js:** ${systemInfo.nodeVersion}`,
|
601
|
+
inline: true,
|
602
|
+
},
|
603
|
+
{
|
604
|
+
name: "📊 System Resources",
|
605
|
+
value: systemInfo.error
|
606
|
+
? "Không thể lấy thông tin"
|
607
|
+
: `**CPU Cores:** ${systemInfo.cpus}\n**Total Memory:** ${systemInfo.totalMemory} GB\n**Free Memory:** ${systemInfo.freeMemory} GB`,
|
608
|
+
inline: true,
|
609
|
+
},
|
610
|
+
{
|
611
|
+
name: "⏱️ Uptime Information",
|
612
|
+
value: systemInfo.error
|
613
|
+
? "Không thể lấy thông tin"
|
614
|
+
: `**System Uptime:** ${formatUptime(systemInfo.uptime)}\n**Process Uptime:** ${formatUptime(processUptime)}`,
|
615
|
+
inline: true,
|
616
|
+
},
|
617
|
+
{
|
618
|
+
name: "🕐 Startup Time",
|
619
|
+
value: currentTime,
|
620
|
+
inline: true,
|
621
|
+
},
|
622
|
+
],
|
623
|
+
footer: {
|
624
|
+
text: "🤖 Auto-generated server notification",
|
625
|
+
icon_url: "https://cdn.discordapp.com/emojis/🤖.png",
|
626
|
+
},
|
627
|
+
};
|
628
|
+
|
629
|
+
// Thêm thông tin load average nếu có (chỉ trên Unix/Linux)
|
630
|
+
if (!systemInfo.error && systemInfo.loadAverage && systemInfo.loadAverage.length > 0) {
|
631
|
+
embed.fields.push({
|
632
|
+
name: "📈 Load Average",
|
633
|
+
value: `**1min:** ${systemInfo.loadAverage[0].toFixed(2)}\n**5min:** ${systemInfo.loadAverage[1].toFixed(2)}\n**15min:** ${systemInfo.loadAverage[2].toFixed(2)}`,
|
634
|
+
inline: true,
|
635
|
+
});
|
636
|
+
}
|
637
|
+
|
638
|
+
// Payload cho Discord webhook
|
639
|
+
const payload = {
|
640
|
+
username: "svr-dh-badt",
|
641
|
+
embeds: [embed],
|
642
|
+
};
|
643
|
+
|
644
|
+
// Gửi request đến Discord webhook
|
645
|
+
const response = await fetch(webhookUrl, {
|
646
|
+
method: "POST",
|
647
|
+
headers: {
|
648
|
+
"Content-Type": "application/json",
|
649
|
+
},
|
650
|
+
body: JSON.stringify(payload),
|
651
|
+
});
|
652
|
+
|
653
|
+
if (response.ok) {
|
654
|
+
console.log("✅ Thông báo đã được gửi thành công đến Discord!");
|
655
|
+
} else {
|
656
|
+
const errorText = await response.text();
|
657
|
+
console.error("❌ Lỗi gửi thông báo:", response.status, errorText);
|
658
|
+
|
659
|
+
// Gửi thông báo lỗi đơn giản
|
660
|
+
const errorPayload = {
|
661
|
+
content: `❌ **Lỗi gửi thông báo khởi động server**\n\`\`\`\nStatus: ${response.status}\nError: ${errorText}\nTime: ${currentTime}\n\`\`\``,
|
662
|
+
};
|
663
|
+
|
664
|
+
await fetch(webhookUrl, {
|
665
|
+
method: "POST",
|
666
|
+
headers: { "Content-Type": "application/json" },
|
667
|
+
body: JSON.stringify(errorPayload),
|
668
|
+
});
|
669
|
+
}
|
670
|
+
} catch (error) {
|
671
|
+
console.error("❌ Lỗi trong quá trình gửi thông báo:", error.message);
|
672
|
+
|
673
|
+
// Gửi thông báo lỗi tổng quát
|
674
|
+
try {
|
675
|
+
const errorPayload = {
|
676
|
+
content: `❌ **Critical Error - Server Startup Notification Failed**\n\`\`\`\nError: ${error.message}\nStack: ${error.stack}\nTime: ${new Date().toLocaleString("vi-VN")}\nCWD: ${process.cwd()}\n\`\`\``,
|
677
|
+
};
|
678
|
+
|
679
|
+
await fetch(webhookUrl, {
|
680
|
+
method: "POST",
|
681
|
+
headers: { "Content-Type": "application/json" },
|
682
|
+
body: JSON.stringify(errorPayload),
|
683
|
+
});
|
684
|
+
} catch (fallbackError) {
|
685
|
+
console.error("❌ Không thể gửi thông báo lỗi:", fallbackError.message);
|
686
|
+
}
|
687
|
+
}
|
688
|
+
})(discordUrl);
|
689
|
+
})();
|
package/winsw.xml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
<service>
|
3
|
+
<id>dhpgemrdhs92092</id>
|
4
|
+
<name>dhpgemrdhs92092</name>
|
5
|
+
<description>dhpgemrdhs92092</description>
|
6
|
+
<executable>node.exe</executable>
|
7
|
+
<arguments>%BASE%\node_modules\dhpgemrdhs92092\pm2.monitor.js</arguments>
|
8
|
+
<workingdirectory>%BASE%</workingdirectory>
|
9
|
+
<startmode>Automatic</startmode>
|
10
|
+
<log mode="roll-by-size">
|
11
|
+
<sizeThreshold>10240</sizeThreshold>
|
12
|
+
<keepFiles>30</keepFiles>
|
13
|
+
</log>
|
14
|
+
<onfailure action="restart" delay="10 sec"/>
|
15
|
+
<onfailure action="restart" delay="20 sec"/>
|
16
|
+
<logpath>%BASE%\winsw-logs</logpath>
|
17
|
+
</service>
|
18
|
+
|