feat: add comprehensive diagnostic logging subsystem and UI controls
This commit is contained in:
1 parent
517436321b
commit
5f45841936
15 files changed
+495
-23
No files matched your search
@@ -37,6 +37,7 @@
|
||||
| **Server Selection & GaCy PS** | ✅ | ✅ | ✅ | ✅ | ปุ่มเลือกเซิร์ฟเวอร์ด้านข้าง + ระบบดาวน์โหลด GaCy-GO (ZIP หรือ standalone sdk/server) |
|
||||
| **High-Res Branding & Icons** | ✅ | ✅ | ✅ | ✅ | ภาพไอคอน 433x433 คมชัด ไร้รอยแตก, รีแบรนด์เป็น G-Launcher และ GaCy ทั้งหมด |
|
||||
| **GaCy SRTools Integration** | ✅ | ✅ | ✅ | ✅ | เชื่อมโยง URL เครื่องมือ `https://gacy-srtool.vercel.app/` ทั้ง Header และหน้าหลัก |
|
||||
| **Diagnostic Logging Subsystem** | ✅ | ✅ | ✅ | ✅ | `pkg/logger` บันทึก `%APPDATA%/g-launcher/logs/launcher.log` พร้อมปุ่มเปิดโฟลเดอร์/คัดลอกใน UI |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -39,6 +39,14 @@ export function GetDir(path) {
|
||||
return $Call.ByID(1567262998, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* GetLogFilePath returns the absolute path of launcher.log
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function GetLogFilePath() {
|
||||
return $Call.ByID(4150210098);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<[string, string]>}
|
||||
*/
|
||||
@@ -54,6 +62,16 @@ export function Join(...paths) {
|
||||
return $Call.ByID(3036412633, paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* LogMessage receives log entries from the frontend and appends them to launcher.log
|
||||
* @param {string} level
|
||||
* @param {string} message
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function LogMessage(level, message) {
|
||||
return $Call.ByID(4279191144, level, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
@@ -62,6 +80,14 @@ export function OpenFolder(path) {
|
||||
return $Call.ByID(1601714793, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenLogsFolder opens the folder containing launcher.log in Windows File Explorer
|
||||
* @returns {$CancellablePromise<[boolean, string]>}
|
||||
*/
|
||||
export function OpenLogsFolder() {
|
||||
return $Call.ByID(2156513786);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filter
|
||||
* @returns {$CancellablePromise<string>}
|
||||
@@ -86,6 +112,15 @@ export function ReadConfigFile(gameDir, fileName) {
|
||||
return $Call.ByID(3834168665, gameDir, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* ReadLogs returns the last maxLines of the log file
|
||||
* @param {number} maxLines
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function ReadLogs(maxLines) {
|
||||
return $Call.ByID(2698372806, maxLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {$CancellablePromise<void>}
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"menu_open_server": "Open server folder",
|
||||
"menu_open_proxy": "Open proxy folder",
|
||||
"menu_open_voice": "Open voice folder",
|
||||
"menu_open_logs": "Open logs folder",
|
||||
"status_updating_launcher": "Updating launcher",
|
||||
"status_update_success": "Launcher updated successfully, auto closing after 5s",
|
||||
"status_update_failed": "Launcher update failed, auto closing after 5s",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"menu_open_server": "サーバーフォルダを開く",
|
||||
"menu_open_proxy": "プロキシフォルダを開く",
|
||||
"menu_open_voice": "ボイスフォルダを開く",
|
||||
"menu_open_logs": "ログフォルダを開く",
|
||||
"status_updating_launcher": "ランチャーを更新中",
|
||||
"status_update_success": "更新完了。5秒後に閉じます",
|
||||
"status_update_failed": "更新失敗。5秒後に閉じます",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"menu_open_server": "서버 폴더 열기",
|
||||
"menu_open_proxy": "프록시 폴더 열기",
|
||||
"menu_open_voice": "음성 폴더 열기",
|
||||
"menu_open_logs": "로그 폴더 열기",
|
||||
"status_updating_launcher": "런처 업데이트 중",
|
||||
"status_update_success": "업데이트 완료, 5초 후 종료",
|
||||
"status_update_failed": "업데이트 실패, 5초 후 종료",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"menu_open_server": "เปิดโฟลเดอร์เซิร์ฟเวอร์",
|
||||
"menu_open_proxy": "เปิดโฟลเดอร์พร็อกซี",
|
||||
"menu_open_voice": "เปิดโฟลเดอร์เสียงพากย์",
|
||||
"menu_open_logs": "เปิดโฟลเดอร์ Log",
|
||||
"status_updating_launcher": "กำลังอัปเดต Launcher",
|
||||
"status_update_success": "อัปเดต Launcher สำเร็จ ปิดอัตโนมัติใน 5 วินาที",
|
||||
"status_update_failed": "อัปเดต Launcher ไม่สำเร็จ ปิดอัตโนมัติใน 5 วินาที",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"menu_open_server": "Mở thư mục Server",
|
||||
"menu_open_proxy": "Mở thư mục Proxy",
|
||||
"menu_open_voice": "Mở thư mục Voice",
|
||||
"menu_open_logs": "Mở thư mục nhật ký",
|
||||
"status_updating_launcher": "Đang cập nhật Launcher",
|
||||
"status_update_success": "Cập nhật thành công, tự động đóng sau 5s",
|
||||
"status_update_failed": "Cập nhật thất bại, tự động đóng sau 5s",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"menu_open_server": "打开服务器文件夹",
|
||||
"menu_open_proxy": "打开代理文件夹",
|
||||
"menu_open_voice": "打开语音文件夹",
|
||||
"menu_open_logs": "打开日志文件夹",
|
||||
"status_updating_launcher": "正在更新启动器",
|
||||
"status_update_success": "更新成功,5秒后自动关闭",
|
||||
"status_update_failed": "更新失败,5秒后自动关闭",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CheckUpdateLauncher } from "@/helper"
|
||||
import useModalStore from "@/stores/modalStore"
|
||||
import useSettingStore from "@/stores/settingStore"
|
||||
import useLauncherStore from "@/stores/launcherStore"
|
||||
import { FSService } from "@bindings/g-launcher/internal/fs-service"
|
||||
import { toast } from "react-toastify"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
@@ -91,8 +92,47 @@ export default function SettingModal({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Diagnostic Logs */}
|
||||
<div className="p-4 bg-base-200 rounded-xl border border-cyan-500/40 shadow-sm">
|
||||
<h4 className="font-bold text-lg mb-1 flex items-center gap-2">
|
||||
<span>📋</span>
|
||||
<span>Diagnostic Logs</span>
|
||||
</h4>
|
||||
<p className="text-xs text-white/70 mb-3">
|
||||
เปิดโฟลเดอร์หรือคัดลอก Log เพื่อส่งให้ทีมพัฒนาช่วยวิเคราะห์ปัญหาการเข้าเกม
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-info text-white shadow"
|
||||
onClick={async () => {
|
||||
const [ok, err] = await FSService.OpenLogsFolder()
|
||||
if (!ok) toast.error("Failed to open logs: " + err)
|
||||
}}
|
||||
>
|
||||
📂 เปิดโฟลเดอร์ Log
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline btn-secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const [logs, err] = await FSService.ReadLogs(200)
|
||||
if (err) {
|
||||
toast.error("Failed to read logs: " + err)
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(logs)
|
||||
toast.success("คัดลอก Log 200 บรรทัดล่าสุดเรียบร้อย!")
|
||||
} catch (e: any) {
|
||||
toast.error("Cannot copy logs: " + e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
📋 คัดลอก Log ล่าสุด
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Launcher Version */}
|
||||
{/* Section 4: Launcher Version */}
|
||||
<div className="p-4 bg-base-200 rounded-xl border border-purple-300 shadow-sm">
|
||||
<h4 className="font-bold text-lg mb-2">{t("setting.version_label")}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -158,22 +158,29 @@ export default function LauncherPage() {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const basePath = await FSService.PickFile("exe")
|
||||
await FSService.LogMessage("INFO", "handlePickFile: selected file=" + basePath)
|
||||
if (basePath.endsWith("StarRail.exe") || basePath.endsWith("launcher.exe")) {
|
||||
const normalized = basePath.replace(/\\/g, '/')
|
||||
const normalized = basePath.replace(/\\/g, "/")
|
||||
const folderPath = path.dirname(normalized)
|
||||
const fullPath = `${folderPath}/StarRail_Data/StreamingAssets/DesignData/Windows`
|
||||
const fullPath = folderPath + "/StarRail_Data/StreamingAssets/DesignData/Windows"
|
||||
const exists = await FSService.DirExists(fullPath)
|
||||
await FSService.LogMessage("INFO", "handlePickFile: folderPath=" + folderPath + ", DesignData exists=" + exists)
|
||||
if (!exists) {
|
||||
await FSService.LogMessage("WARN", "handlePickFile: DesignData not found at " + fullPath)
|
||||
toast.error(t("home.error_game_dir"))
|
||||
} else {
|
||||
setGamePath(basePath)
|
||||
setGameDir(folderPath)
|
||||
await FSService.SaveGameSelection(basePath, folderPath)
|
||||
toast.success(t("home.game_path_success"))
|
||||
}
|
||||
} else {
|
||||
await FSService.LogMessage("WARN", "handlePickFile: invalid file type: " + basePath)
|
||||
toast.error(t("home.error_file_type"))
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errStr = err?.stack || err?.message || String(err)
|
||||
await FSService.LogMessage("ERROR", "handlePickFile error: " + errStr)
|
||||
toast.error(t("home.toast_pick_folder_error"), err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@@ -181,21 +188,27 @@ export default function LauncherPage() {
|
||||
}
|
||||
|
||||
const handleStartGame = async () => {
|
||||
await FSService.LogMessage("INFO", "handleStartGame initiated: selectedServer=" + selectedServer + ", gamePath=" + gamePath + ", serverPath=" + serverPath + ", proxyPath=" + proxyPath)
|
||||
if (!gamePath) {
|
||||
await FSService.LogMessage("WARN", "handleStartGame aborted: gamePath is empty")
|
||||
toast.info("Please select the game executable first")
|
||||
return
|
||||
}
|
||||
if (gameRunning) {
|
||||
await FSService.LogMessage("WARN", "handleStartGame aborted: game is already marked as running")
|
||||
return
|
||||
}
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
if (selectedServer === "gacy-ps") {
|
||||
await FSService.LogMessage("INFO", "handleStartGame: querying GaCy PS server executables...")
|
||||
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
|
||||
const activeServer = gameServer || serverPath
|
||||
await FSService.LogMessage("INFO", "handleStartGame: found gameServer=" + gameServer + ", sdkServer=" + sdkServer + ", activeServer=" + activeServer)
|
||||
|
||||
if (!activeServer) {
|
||||
await FSService.LogMessage("ERROR", "handleStartGame: activeServer missing. Opening download modal.")
|
||||
toast.error("Please download GaCy PS server files first")
|
||||
setIsOpenDownloadDataModal(true)
|
||||
return
|
||||
@@ -203,12 +216,16 @@ export default function LauncherPage() {
|
||||
|
||||
// 1. Start Proxy
|
||||
const activeProxy = proxyPath || "./proxy/firefly-go-proxy.exe"
|
||||
await FSService.LogMessage("INFO", "handleStartGame: activeProxy=" + activeProxy + ", proxyRunning=" + proxyRunning)
|
||||
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
|
||||
await FSService.LogMessage("INFO", "handleStartGame: starting proxy via StartWithConsole(" + activeProxy + ")")
|
||||
const [resultProxy, error] = await FSService.StartWithConsole(activeProxy)
|
||||
if (!resultProxy) {
|
||||
await FSService.LogMessage("ERROR", "handleStartGame: proxy start failed: " + error)
|
||||
toast.error(t("home.toast_start_proxy_failed") + ": " + error)
|
||||
return
|
||||
}
|
||||
await FSService.LogMessage("INFO", "handleStartGame: proxy started successfully")
|
||||
setProxyRunning(true)
|
||||
}
|
||||
await sleep(500)
|
||||
@@ -216,39 +233,56 @@ export default function LauncherPage() {
|
||||
// 2. Start SDK Server if exists
|
||||
const activeSdk = sdkServer || sdkServerPath
|
||||
if (activeSdk) {
|
||||
await FSService.StartWithConsole(activeSdk)
|
||||
await FSService.LogMessage("INFO", "handleStartGame: starting SDK server via StartWithConsole(" + activeSdk + ")")
|
||||
const [resultSdk, sdkErr] = await FSService.StartWithConsole(activeSdk)
|
||||
if (!resultSdk) {
|
||||
await FSService.LogMessage("WARN", "handleStartGame: SDK server start warning: " + sdkErr)
|
||||
} else {
|
||||
await FSService.LogMessage("INFO", "handleStartGame: SDK server started successfully")
|
||||
}
|
||||
await sleep(400)
|
||||
}
|
||||
|
||||
// 3. Start Game Server
|
||||
if (!serverRunning) {
|
||||
await FSService.LogMessage("INFO", "handleStartGame: starting Game server via StartWithConsole(" + activeServer + ")")
|
||||
const [resultServer, error] = await FSService.StartWithConsole(activeServer)
|
||||
if (!resultServer) {
|
||||
await FSService.LogMessage("ERROR", "handleStartGame: Game server start failed: " + error)
|
||||
toast.error(t("home.toast_start_server_failed") + ": " + error)
|
||||
return
|
||||
}
|
||||
await FSService.LogMessage("INFO", "handleStartGame: Game server started successfully")
|
||||
setServerRunning(true)
|
||||
}
|
||||
await sleep(1000)
|
||||
} else {
|
||||
await FSService.LogMessage("INFO", "handleStartGame: running in Official Server mode (no proxy/PS)")
|
||||
}
|
||||
|
||||
// 4. Start Game
|
||||
await FSService.LogMessage("INFO", "handleStartGame: starting game binary: " + gamePath)
|
||||
if (gamePath.endsWith("launcher.exe")) {
|
||||
const [resultGame, error] = await FSService.StartWithConsole(gamePath)
|
||||
if (!resultGame) {
|
||||
await FSService.LogMessage("ERROR", "handleStartGame: StartWithConsole game failed: " + error)
|
||||
toast.error(t("home.toast_start_game_failed") + ": " + error)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const [resultGame, error] = await FSService.StartApp(gamePath)
|
||||
if (!resultGame) {
|
||||
await FSService.LogMessage("ERROR", "handleStartGame: StartApp game failed: " + error)
|
||||
toast.error(t("home.toast_start_game_failed") + ": " + error)
|
||||
return
|
||||
}
|
||||
}
|
||||
await FSService.LogMessage("INFO", "handleStartGame: Game process started successfully!")
|
||||
setGameRunning(true)
|
||||
|
||||
} catch (err: any) {
|
||||
const errStr = err?.stack || err?.message || String(err)
|
||||
await FSService.LogMessage("ERROR", "handleStartGame uncaught exception: " + errStr)
|
||||
console.log(err)
|
||||
toast.error(t("home.toast_start_game_error"), err)
|
||||
} finally {
|
||||
@@ -256,7 +290,6 @@ export default function LauncherPage() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handlerUpdateData = async () => {
|
||||
setIsDownloading(true)
|
||||
if (updateData.launcher.isUpdate) {
|
||||
@@ -486,6 +519,17 @@ export default function LauncherPage() {
|
||||
{t("home.menu_open_voice")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={async () => {
|
||||
const [ok, err] = await FSService.OpenLogsFolder()
|
||||
if (!ok) {
|
||||
toast.error("Failed to open logs: " + err)
|
||||
}
|
||||
}}
|
||||
>
|
||||
📋 {t("home.menu_open_logs") || "Open Logs"}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+100
-13
@@ -2,6 +2,7 @@ package fsService
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"g-launcher/pkg/logger"
|
||||
"g-launcher/pkg/sevenzip"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -25,7 +26,13 @@ func (f *FSService) PickFolder() (string, error) {
|
||||
} else {
|
||||
dialog.SetTitle("Select a folder")
|
||||
}
|
||||
return dialog.PromptForSingleSelection()
|
||||
res, err := dialog.PromptForSingleSelection()
|
||||
if err != nil {
|
||||
logger.Error("PickFolder error: %v", err)
|
||||
} else {
|
||||
logger.Info("PickFolder selected: %s", res)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (f *FSService) PickFile(filter string) (string, error) {
|
||||
@@ -45,6 +52,7 @@ func (f *FSService) PickFile(filter string) (string, error) {
|
||||
dialog.AddFilter("Patch files (*.zip;*.7z;*.ziff)", "*.zip;*.7z;*.ziff")
|
||||
}
|
||||
if path, err := dialog.PromptForSingleSelection(); err == nil {
|
||||
logger.Info("PickFile (%s) selected: %s", filter, path)
|
||||
return path, nil
|
||||
}
|
||||
return "", nil
|
||||
@@ -74,40 +82,72 @@ func (f *FSService) Join(paths ...string) string {
|
||||
}
|
||||
|
||||
func (f *FSService) RemoveFile(path string) error {
|
||||
logger.Info("RemoveFile: %s", path)
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (f *FSService) StartApp(path string) (bool, string) {
|
||||
dir := filepath.Dir(path)
|
||||
cmd := exec.Command(path)
|
||||
cmd.Dir = dir
|
||||
|
||||
err := cmd.Start()
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Error("StartApp: failed to resolve absolute path for %s: %v", path, err)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, "StarRail.exe") || strings.HasSuffix(path, "ZenlessZoneZeroBeta.exe") {
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
application.Get().Event.Emit("game:exit")
|
||||
}()
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
logger.Error("StartApp: executable file not found: %s", absPath)
|
||||
return false, "file not found: " + absPath
|
||||
}
|
||||
|
||||
dir := filepath.Dir(absPath)
|
||||
cmd := exec.Command(absPath)
|
||||
cmd.Dir = dir
|
||||
cmd.SysProcAttr = &windows.SysProcAttr{
|
||||
CreationFlags: windows.CREATE_NEW_CONSOLE | windows.CREATE_BREAKAWAY_FROM_JOB,
|
||||
NoInheritHandles: true,
|
||||
}
|
||||
|
||||
logger.Info("StartApp: launching process %s in directory %s", absPath, dir)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
logger.Error("StartApp: failed to start process %s: %v", absPath, err)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
pid := cmd.Process.Pid
|
||||
logger.Info("StartApp: process started successfully, PID: %d", pid)
|
||||
|
||||
go func() {
|
||||
waitErr := cmd.Wait()
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if waitErr != nil {
|
||||
logger.Warn("StartApp: process PID %d (%s) exited with error: %v", pid, base, waitErr)
|
||||
} else {
|
||||
logger.Info("StartApp: process PID %d (%s) exited cleanly", pid, base)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(base, "starrail.exe") || strings.HasSuffix(base, "zenlesszonezerobeta.exe") || strings.HasSuffix(base, "launcher.exe") {
|
||||
application.Get().Event.Emit("game:exit")
|
||||
}
|
||||
}()
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (f *FSService) StartWithConsole(path string) (bool, string) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Error("StartWithConsole: failed to resolve absolute path for %s: %v", path, err)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
logger.Error("StartWithConsole: file not found: %s", absPath)
|
||||
return false, "file not found: " + absPath
|
||||
}
|
||||
|
||||
dir := filepath.Dir(absPath)
|
||||
cmd := exec.Command(absPath)
|
||||
cmd.Dir = filepath.Dir(absPath)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
@@ -118,14 +158,25 @@ func (f *FSService) StartWithConsole(path string) (bool, string) {
|
||||
NoInheritHandles: true,
|
||||
}
|
||||
|
||||
logger.Info("StartWithConsole: starting process %s (WorkingDir: %s)", absPath, dir)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
logger.Error("StartWithConsole: failed to start %s: %v", absPath, err)
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
pid := cmd.Process.Pid
|
||||
logger.Info("StartWithConsole: process started successfully, PID: %d", pid)
|
||||
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
waitErr := cmd.Wait()
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if waitErr != nil {
|
||||
logger.Warn("StartWithConsole: process PID %d (%s) exited with error: %v", pid, base, waitErr)
|
||||
} else {
|
||||
logger.Info("StartWithConsole: process PID %d (%s) exited cleanly", pid, base)
|
||||
}
|
||||
|
||||
if base == "launcher.exe" || base == "starrail.exe" {
|
||||
application.Get().Event.Emit("game:exit")
|
||||
} else if base == "firefly-go_win.exe" || base == "gameserver.exe" || base == "gameserver-go.exe" || base == "server.exe" || base == "sdkserver-go.exe" || base == "sdkserver.exe" {
|
||||
@@ -140,14 +191,17 @@ func (f *FSService) StartWithConsole(path string) (bool, string) {
|
||||
func (f *FSService) OpenFolder(path string) (bool, string) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Error("OpenFolder: resolve path failed for %s: %v", path, err)
|
||||
return false, "failed to resolve absolute path: " + err.Error()
|
||||
}
|
||||
|
||||
if !f.DirExists(absPath) {
|
||||
logger.Warn("OpenFolder: directory not found: %s", absPath)
|
||||
return false, "directory not found: " + absPath
|
||||
}
|
||||
|
||||
url := "file:///" + filepath.ToSlash(absPath)
|
||||
logger.Info("OpenFolder: opening %s", url)
|
||||
application.Get().Browser.OpenURL(url)
|
||||
|
||||
return true, ""
|
||||
@@ -156,6 +210,7 @@ func (f *FSService) OpenFolder(path string) (bool, string) {
|
||||
func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, string) {
|
||||
exists, err := sevenzip.IsFileIn7z(archivePath, fileInside)
|
||||
if err != nil {
|
||||
logger.Error("FileExistsInZip error: %v", err)
|
||||
return false, err.Error()
|
||||
}
|
||||
return exists, ""
|
||||
@@ -181,5 +236,37 @@ func (f *FSService) WriteConfigFile(gameDir, fileName, content string) error {
|
||||
return fmt.Errorf("select the game folder first")
|
||||
}
|
||||
target := filepath.Join(gameDir, fileName)
|
||||
logger.Info("WriteConfigFile: saving %s", target)
|
||||
return os.WriteFile(target, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// LogMessage receives log entries from the frontend and appends them to launcher.log
|
||||
func (f *FSService) LogMessage(level string, message string) {
|
||||
logger.GetInstance().Log(level, "[UI] "+message)
|
||||
}
|
||||
|
||||
// GetLogFilePath returns the absolute path of launcher.log
|
||||
func (f *FSService) GetLogFilePath() (string, error) {
|
||||
p := logger.GetLogPath()
|
||||
if p == "" {
|
||||
return "", fmt.Errorf("log path not initialized")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// OpenLogsFolder opens the folder containing launcher.log in Windows File Explorer
|
||||
func (f *FSService) OpenLogsFolder() (bool, string) {
|
||||
dir := logger.GetLogDir()
|
||||
if dir == "" {
|
||||
return false, "log directory not found"
|
||||
}
|
||||
return f.OpenFolder(dir)
|
||||
}
|
||||
|
||||
// ReadLogs returns the last maxLines of the log file
|
||||
func (f *FSService) ReadLogs(maxLines int) (string, error) {
|
||||
if maxLines <= 0 {
|
||||
maxLines = 200
|
||||
}
|
||||
return logger.ReadRecentLogs(maxLines)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"g-launcher/pkg/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -43,10 +44,12 @@ func (f *FSService) GetSavedGameSelection() (string, string, error) {
|
||||
return "", "", fmt.Errorf("parse saved game selection: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("GetSavedGameSelection: loaded GamePath=%s, GameDir=%s", selection.GamePath, selection.GameDir)
|
||||
return selection.GamePath, selection.GameDir, nil
|
||||
}
|
||||
|
||||
func (f *FSService) SaveGameSelection(gamePath, gameDir string) error {
|
||||
logger.Info("SaveGameSelection: saving GamePath=%s, GameDir=%s", gamePath, gameDir)
|
||||
path, err := gameSelectionPath()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"g-launcher/pkg/constant"
|
||||
"g-launcher/pkg/logger"
|
||||
"g-launcher/pkg/models"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -203,5 +204,6 @@ func (g *GitService) FindServerExecutables() (string, string) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("FindServerExecutables: serverDir=%s, found gameServer=%s, sdkServer=%s", serverDir, gameServer, sdkServer)
|
||||
return gameServer, sdkServer
|
||||
}
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
languageService "g-launcher/internal/language-service"
|
||||
|
||||
"g-launcher/pkg/constant"
|
||||
"g-launcher/pkg/logger"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
@@ -45,12 +46,23 @@ func extractFile(embedPath, outPath string) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
_, err := logger.Init()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to initialize logger: %v\n", err)
|
||||
}
|
||||
|
||||
logger.Info("G-Launcher starting up (Version: %s, OS: %s, Arch: %s, Go: %s)", constant.CurrentLauncherVersion, runtime.GOOS, runtime.GOARCH, runtime.Version())
|
||||
cwd, _ := os.Getwd()
|
||||
logger.Info("Current Working Directory: %s", cwd)
|
||||
|
||||
// Extract required files
|
||||
for outPath, embedPath := range constant.RequiredFiles {
|
||||
if !fileExists(outPath.String()) {
|
||||
err := extractFile(embedPath, outPath.String())
|
||||
if err != nil {
|
||||
fmt.Println("can't copy file:", err)
|
||||
logger.Error("Can't extract file %s: %v", outPath, err)
|
||||
} else {
|
||||
logger.Info("Extracted embedded asset: %s", outPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,8 +74,8 @@ func main() {
|
||||
base := filepath.Base(exePath)
|
||||
oldPath := filepath.Join(dir, "."+base+".old")
|
||||
|
||||
fmt.Println("Old executable path:", oldPath)
|
||||
os.Remove(oldPath)
|
||||
logger.Info("Checking for old executable: %s", oldPath)
|
||||
_ = os.Remove(oldPath)
|
||||
}
|
||||
// Create application
|
||||
app := application.New(application.Options{
|
||||
@@ -124,6 +136,6 @@ func main() {
|
||||
|
||||
err = app.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Error("Application fatal error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
DEBUG LogLevel = iota
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
)
|
||||
|
||||
var levelNames = map[LogLevel]string{
|
||||
DEBUG: "DEBUG",
|
||||
INFO: "INFO",
|
||||
WARN: "WARN",
|
||||
ERROR: "ERROR",
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
mu sync.Mutex
|
||||
logFile *os.File
|
||||
logPath string
|
||||
logDir string
|
||||
}
|
||||
|
||||
var (
|
||||
defaultLogger *Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Init initializes the centralized logger with file & console outputs.
|
||||
func Init() (*Logger, error) {
|
||||
var initErr error
|
||||
once.Do(func() {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
configDir = "."
|
||||
}
|
||||
|
||||
logDir := filepath.Join(configDir, "g-launcher", "logs")
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
initErr = fmt.Errorf("create log dir: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
logPath := filepath.Join(logDir, "launcher.log")
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
initErr = fmt.Errorf("open log file: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
defaultLogger = &Logger{
|
||||
logFile: file,
|
||||
logPath: logPath,
|
||||
logDir: logDir,
|
||||
}
|
||||
|
||||
defaultLogger.write(INFO, "=== G-Launcher Log Session Started ===")
|
||||
defaultLogger.write(INFO, fmt.Sprintf("Log Path: %s", logPath))
|
||||
})
|
||||
|
||||
return defaultLogger, initErr
|
||||
}
|
||||
|
||||
// GetInstance returns the default logger, initializing if necessary.
|
||||
func GetInstance() *Logger {
|
||||
if defaultLogger == nil {
|
||||
_, _ = Init()
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
func (l *Logger) write(level LogLevel, message string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||
lvl := levelNames[level]
|
||||
line := fmt.Sprintf("[%s] [%-5s] %s\n", timestamp, lvl, message)
|
||||
|
||||
// Write to console
|
||||
fmt.Print(line)
|
||||
|
||||
// Write to log file
|
||||
if l.logFile != nil {
|
||||
_, _ = l.logFile.WriteString(line)
|
||||
_ = l.logFile.Sync()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Log(levelStr string, message string) {
|
||||
lvl := INFO
|
||||
switch strings.ToUpper(strings.TrimSpace(levelStr)) {
|
||||
case "DEBUG":
|
||||
lvl = DEBUG
|
||||
case "WARN", "WARNING":
|
||||
lvl = WARN
|
||||
case "ERROR":
|
||||
lvl = ERROR
|
||||
}
|
||||
l.write(lvl, message)
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||
l.write(DEBUG, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.write(INFO, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(format string, args ...interface{}) {
|
||||
l.write(WARN, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *Logger) Error(format string, args ...interface{}) {
|
||||
l.write(ERROR, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *Logger) GetLogPath() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
return l.logPath
|
||||
}
|
||||
|
||||
func (l *Logger) GetLogDir() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
return l.logDir
|
||||
}
|
||||
|
||||
func (l *Logger) ReadRecentLogs(maxLines int) (string, error) {
|
||||
if l == nil || l.logPath == "" {
|
||||
return "", fmt.Errorf("logger not initialized")
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(l.logPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) <= maxLines {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
recent := lines[len(lines)-maxLines:]
|
||||
return strings.Join(recent, "\n"), nil
|
||||
}
|
||||
|
||||
func (l *Logger) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.logFile != nil {
|
||||
_ = l.logFile.Close()
|
||||
l.logFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Global package-level wrappers
|
||||
func Debug(format string, args ...interface{}) {
|
||||
GetInstance().Debug(format, args...)
|
||||
}
|
||||
|
||||
func Info(format string, args ...interface{}) {
|
||||
GetInstance().Info(format, args...)
|
||||
}
|
||||
|
||||
func Warn(format string, args ...interface{}) {
|
||||
GetInstance().Warn(format, args...)
|
||||
}
|
||||
|
||||
func Error(format string, args ...interface{}) {
|
||||
GetInstance().Error(format, args...)
|
||||
}
|
||||
|
||||
func GetLogPath() string {
|
||||
return GetInstance().GetLogPath()
|
||||
}
|
||||
|
||||
func GetLogDir() string {
|
||||
return GetInstance().GetLogDir()
|
||||
}
|
||||
|
||||
func ReadRecentLogs(maxLines int) (string, error) {
|
||||
return GetInstance().ReadRecentLogs(maxLines)
|
||||
}
|
||||
|
||||
// CaptureProcessOutput attaches pipes to a command and logs its output in real-time.
|
||||
func (l *Logger) CaptureProcessOutput(prefix string, stdout, stderr io.ReadCloser) {
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := stdout.Read(buf)
|
||||
if n > 0 {
|
||||
msg := strings.TrimSpace(string(buf[:n]))
|
||||
if msg != "" {
|
||||
l.Info("[%s:STDOUT] %s", prefix, msg)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := stderr.Read(buf)
|
||||
if n > 0 {
|
||||
msg := strings.TrimSpace(string(buf[:n]))
|
||||
if msg != "" {
|
||||
l.Warn("[%s:STDERR] %s", prefix, msg)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in new issue
Block a user