mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-25 22:31:46 -03:00
Add Send Logs button for one-click log submission to support
Collects launcher logs, game client logs, and config snapshot into a ZIP file and uploads to auth server. Shows submission ID for sharing with support. Includes i18n for all 11 locales.
This commit is contained in:
238
backend/utils/logCollector.js
Normal file
238
backend/utils/logCollector.js
Normal file
@@ -0,0 +1,238 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const zlib = require('zlib');
|
||||
const logger = require('../logger');
|
||||
|
||||
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB per file
|
||||
|
||||
/**
|
||||
* Get the HytaleSaves directory (game client logs)
|
||||
*/
|
||||
function getHytaleSavesDir() {
|
||||
const home = os.homedir();
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
|
||||
} else if (process.platform === 'darwin') {
|
||||
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
|
||||
} else {
|
||||
return path.join(home, '.hytalesaves');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a log file, capping at MAX_FILE_SIZE (keeps tail/most recent lines)
|
||||
*/
|
||||
function readLogFile(filePath) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.size <= MAX_FILE_SIZE) {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
// Read only the last MAX_FILE_SIZE bytes
|
||||
const fd = fs.openSync(filePath, 'r');
|
||||
const buffer = Buffer.alloc(MAX_FILE_SIZE);
|
||||
fs.readSync(fd, buffer, 0, MAX_FILE_SIZE, stats.size - MAX_FILE_SIZE);
|
||||
fs.closeSync(fd);
|
||||
|
||||
const content = buffer.toString('utf8');
|
||||
// Skip first partial line
|
||||
const firstNewline = content.indexOf('\n');
|
||||
const trimmed = firstNewline >= 0 ? content.substring(firstNewline + 1) : content;
|
||||
return `[... truncated ${stats.size - MAX_FILE_SIZE} bytes ...]\n` + trimmed;
|
||||
} catch (err) {
|
||||
return `[Error reading file: ${err.message}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files matching a date pattern from a directory
|
||||
*/
|
||||
function getFilesForDate(dir, dateStr, pattern) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
|
||||
try {
|
||||
return fs.readdirSync(dir)
|
||||
.filter(f => f.includes(dateStr) && (pattern ? pattern.test(f) : true))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: path.join(dir, f),
|
||||
mtime: fs.statSync(path.join(dir, f)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ZIP BUILDER (pure Node.js, no dependencies)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* CRC32 lookup table
|
||||
*/
|
||||
const crc32Table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let c = i;
|
||||
for (let j = 0; j < 8; j++) {
|
||||
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
}
|
||||
crc32Table[i] = c;
|
||||
}
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xFFFFFFFF;
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
crc = crc32Table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ZIP file from an array of {name, content} entries
|
||||
* Uses DEFLATE compression via built-in zlib
|
||||
*/
|
||||
function createZipBuffer(files) {
|
||||
const localHeaders = [];
|
||||
const centralEntries = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const nameBytes = Buffer.from(file.name, 'utf8');
|
||||
const contentBytes = Buffer.from(file.content, 'utf8');
|
||||
const compressed = zlib.deflateRawSync(contentBytes);
|
||||
const crcVal = crc32(contentBytes);
|
||||
|
||||
// Local file header (30 bytes + filename)
|
||||
const local = Buffer.alloc(30);
|
||||
local.writeUInt32LE(0x04034b50, 0); // signature
|
||||
local.writeUInt16LE(20, 4); // version needed
|
||||
local.writeUInt16LE(0, 6); // flags
|
||||
local.writeUInt16LE(8, 8); // compression: DEFLATE
|
||||
local.writeUInt16LE(0, 10); // mod time
|
||||
local.writeUInt16LE(0, 12); // mod date
|
||||
local.writeUInt32LE(crcVal, 14); // crc32
|
||||
local.writeUInt32LE(compressed.length, 18); // compressed size
|
||||
local.writeUInt32LE(contentBytes.length, 22); // uncompressed size
|
||||
local.writeUInt16LE(nameBytes.length, 26); // filename length
|
||||
local.writeUInt16LE(0, 28); // extra field length
|
||||
|
||||
localHeaders.push(local, nameBytes, compressed);
|
||||
|
||||
// Central directory entry (46 bytes + filename)
|
||||
const central = Buffer.alloc(46);
|
||||
central.writeUInt32LE(0x02014b50, 0); // signature
|
||||
central.writeUInt16LE(20, 4); // version made by
|
||||
central.writeUInt16LE(20, 6); // version needed
|
||||
central.writeUInt16LE(0, 8); // flags
|
||||
central.writeUInt16LE(8, 10); // compression
|
||||
central.writeUInt16LE(0, 12); // mod time
|
||||
central.writeUInt16LE(0, 14); // mod date
|
||||
central.writeUInt32LE(crcVal, 16); // crc32
|
||||
central.writeUInt32LE(compressed.length, 20); // compressed size
|
||||
central.writeUInt32LE(contentBytes.length, 24); // uncompressed size
|
||||
central.writeUInt16LE(nameBytes.length, 28); // filename length
|
||||
central.writeUInt16LE(0, 30); // extra field length
|
||||
central.writeUInt16LE(0, 32); // comment length
|
||||
central.writeUInt16LE(0, 34); // disk number
|
||||
central.writeUInt16LE(0, 36); // internal attrs
|
||||
central.writeUInt32LE(0, 38); // external attrs
|
||||
central.writeUInt32LE(offset, 42); // local header offset
|
||||
|
||||
centralEntries.push(central, nameBytes);
|
||||
offset += 30 + nameBytes.length + compressed.length;
|
||||
}
|
||||
|
||||
const centralDirBuf = Buffer.concat(centralEntries);
|
||||
|
||||
// End of central directory (22 bytes)
|
||||
const eocd = Buffer.alloc(22);
|
||||
eocd.writeUInt32LE(0x06054b50, 0); // signature
|
||||
eocd.writeUInt16LE(0, 4); // disk number
|
||||
eocd.writeUInt16LE(0, 6); // cd disk number
|
||||
eocd.writeUInt16LE(files.length, 8); // entries on disk
|
||||
eocd.writeUInt16LE(files.length, 10); // total entries
|
||||
eocd.writeUInt32LE(centralDirBuf.length, 12); // cd size
|
||||
eocd.writeUInt32LE(offset, 16); // cd offset
|
||||
eocd.writeUInt16LE(0, 20); // comment length
|
||||
|
||||
return Buffer.concat([...localHeaders, centralDirBuf, eocd]);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Collect all relevant logs for submission
|
||||
* Returns { files: [{name, content}], meta: {username, platform, version} }
|
||||
*/
|
||||
function collectLogs() {
|
||||
const files = [];
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
// 1. Launcher logs
|
||||
const launcherLogDir = logger.getLogDirectory();
|
||||
if (launcherLogDir && fs.existsSync(launcherLogDir)) {
|
||||
// Today's launcher logs
|
||||
const todayLogs = getFilesForDate(launcherLogDir, todayStr, /^launcher-.*\.log$/);
|
||||
for (const f of todayLogs) {
|
||||
files.push({ name: `launcher/${f.name}`, content: readLogFile(f.path) });
|
||||
}
|
||||
|
||||
// Most recent from yesterday (just one)
|
||||
const yesterdayLogs = getFilesForDate(launcherLogDir, yesterdayStr, /^launcher-.*\.log$/);
|
||||
if (yesterdayLogs.length > 0) {
|
||||
files.push({ name: `launcher/${yesterdayLogs[0].name}`, content: readLogFile(yesterdayLogs[0].path) });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Game client logs
|
||||
const savesDir = getHytaleSavesDir();
|
||||
const clientLogDir = path.join(savesDir, 'Logs');
|
||||
if (fs.existsSync(clientLogDir)) {
|
||||
const clientLogs = getFilesForDate(clientLogDir, todayStr, /client\.log$/);
|
||||
for (const f of clientLogs) {
|
||||
files.push({ name: `client/${f.name}`, content: readLogFile(f.path) });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Config snapshot
|
||||
const appDir = logger.getAppDir ? logger.getAppDir() : logger.getInstallPath();
|
||||
const configPath = path.join(appDir, 'config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
files.push({ name: 'config.json', content: configContent });
|
||||
} catch (err) {
|
||||
files.push({ name: 'config.json', content: `[Error reading config: ${err.message}]` });
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata
|
||||
const { app } = require('electron');
|
||||
let username = 'unknown';
|
||||
try {
|
||||
const configFile = path.join(appDir, 'config.json');
|
||||
if (fs.existsSync(configFile)) {
|
||||
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||
username = cfg.username || cfg.playerName || 'unknown';
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return {
|
||||
files,
|
||||
meta: {
|
||||
username,
|
||||
platform: `${process.platform}-${process.arch}`,
|
||||
version: app.getVersion()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { collectLogs, createZipBuffer };
|
||||
Reference in New Issue
Block a user