package fsService import ( "fmt" "g-launcher/pkg/logger" "g-launcher/pkg/sevenzip" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/wailsapp/wails/v3/pkg/application" "golang.org/x/sys/windows" ) type FSService struct{} func (f *FSService) PickFolder() (string, error) { dialog := application.Get().Dialog.OpenFile(). CanChooseDirectories(true). CanChooseFiles(false). ResolvesAliases(true) if runtime.GOOS == "darwin" { dialog.SetMessage("Select a folder") } else { dialog.SetTitle("Select a folder") } 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) { dialog := application.Get().Dialog.OpenFile(). CanChooseFiles(true). ResolvesAliases(true) if runtime.GOOS == "darwin" { dialog.SetMessage("Select a file") } else { dialog.SetTitle("Select a file") } if filter == "exe" { dialog.AddFilter("Executable Files (*.exe)", "*.exe") } else if filter == "zip" { dialog.AddFilter("ZIP archives (*.zip)", "*.zip") } else if filter == "patch" { 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 } func (f *FSService) DirExists(path string) bool { info, err := os.Stat(path) if err != nil { return false } return info.IsDir() } func (f *FSService) FileExists(path string) bool { if info, err := os.Stat(path); err == nil { return info.Mode().IsRegular() } return false } func (f *FSService) GetDir(path string) string { return filepath.Dir(path) } func (f *FSService) Join(paths ...string) string { return filepath.Join(paths...) } func (f *FSService) RemoveFile(path string) error { logger.Info("RemoveFile: %s", path) return os.Remove(path) } func (f *FSService) StartApp(path string) (bool, string) { 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 _, 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 = dir cmd.Stdin = nil cmd.Stdout = nil cmd.Stderr = nil cmd.SysProcAttr = &windows.SysProcAttr{ CreationFlags: windows.CREATE_NEW_CONSOLE | windows.CREATE_BREAKAWAY_FROM_JOB, 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() { 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" { application.Get().Event.Emit("server:exit") } else if base == "firefly-go-proxy.exe" || base == "proxy.exe" { application.Get().Event.Emit("proxy:exit") } }() return true, "" } 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, "" } 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, "" } func (f *FSService) ReadConfigFile(gameDir, fileName string) (string, error) { if strings.TrimSpace(gameDir) == "" { return "", fmt.Errorf("select the game folder first") } target := filepath.Join(gameDir, fileName) content, err := os.ReadFile(target) if err != nil { if os.IsNotExist(err) { return "", nil } return "", fmt.Errorf("read file %s: %w", target, err) } return string(content), nil } func (f *FSService) WriteConfigFile(gameDir, fileName, content string) error { if strings.TrimSpace(gameDir) == "" { 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) }