diff --git a/FyneApp.toml b/FyneApp.toml index 1896a61..1185d77 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -2,5 +2,5 @@ Icon = "Icon.png" Name = "TurtleSilicon" ID = "com.tairasu.turtlesilicon" - Version = "1.0.7" - Build = 12 + Version = "1.1.0" + Build = 13 diff --git a/README.md b/README.md index a8a9a9e..ada3638 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,14 @@ All credit for the core translation layer `winerosetta` and `rosettax87` goes to * Click "Patch TurtleWoW". * Click "Patch CrossOver". * Status indicators will turn green once patching is successful for each. -6. **Launch Game**: - * Once both paths are set and both components are patched, the "Launch Game" button will become active. Click it. - * Follow the on-screen prompts (you will need to enter your password in a new Terminal window for `rosettax87`). -7. **Enjoy**: Experience a significantly smoother Turtle WoW on your Apple Silicon Mac! +6. **Start RosettaX87 Service**: + * Click "Start RosettaX87 Service" and enter your sudo password when prompted. + * This will run the RosettaX87 service in the background and is required for launching the game. + * The service will automatically stop when you close the launcher. +7. **Launch Game**: + * Once both paths are set, both components are patched, and the RosettaX87 service is running, the "Launch Game" button will become active. Click it. + * The game will launch directly without requiring additional password prompts. +8. **Enjoy**: Experience a significantly smoother Turtle WoW on your Apple Silicon Mac! ### Method 2: Running from Source Code diff --git a/main.go b/main.go index d9b0579..a668272 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "turtlesilicon/pkg/service" "turtlesilicon/pkg/ui" "turtlesilicon/pkg/utils" @@ -14,7 +15,7 @@ import ( "fyne.io/fyne/v2/widget" ) -const appVersion = "1.0.7" +const appVersion = "1.1.0" func main() { myApp := app.NewWithID("com.tairasu.turtlesilicon") @@ -53,5 +54,12 @@ func main() { content := ui.CreateUI(myWindow) myWindow.SetContent(content) + // Set up cleanup when window closes + myWindow.SetCloseIntercept(func() { + log.Println("Application closing, cleaning up RosettaX87 service...") + service.CleanupService() + myApp.Quit() + }) + myWindow.ShowAndRun() } diff --git a/pkg/launcher/launcher.go b/pkg/launcher/launcher.go index 0534113..25c1acd 100644 --- a/pkg/launcher/launcher.go +++ b/pkg/launcher/launcher.go @@ -56,59 +56,39 @@ func LaunchGame(myWindow fyne.Window) { return } - appleScriptSafeRosettaDir := utils.EscapeStringForAppleScript(rosettaInTurtlePath) - cmd1Script := fmt.Sprintf("tell application \"Terminal\" to do script \"cd \" & quoted form of \"%s\" & \" && sudo ./rosettax87\"", appleScriptSafeRosettaDir) + // Since RosettaX87 service is already running, we can directly launch WoW + log.Println("RosettaX87 service is running. Proceeding to launch WoW.") - log.Println("Launching rosettax87 (requires sudo password in new terminal)...") - if !utils.RunOsascript(cmd1Script, myWindow) { + if paths.CrossoverPath == "" || paths.TurtlewowPath == "" { + dialog.ShowError(fmt.Errorf("CrossOver path or TurtleWoW path is not set. Cannot launch WoW."), myWindow) return } - dialog.ShowConfirm("Action Required", - "The rosetta x87 terminal has been initiated.\n\n"+ - "1. Please enter your sudo password in that new terminal window.\n"+ - "2. Wait for rosetta x87 to fully start.\n\n"+ - "Click Yes once rosetta x87 is running and you have entered the password.\n"+ - "Click No to abort launching WoW.", - func(confirmed bool) { - if confirmed { - log.Println("User confirmed rosetta x87 is running. Proceeding to launch WoW.") - if paths.CrossoverPath == "" || paths.TurtlewowPath == "" { - dialog.ShowError(fmt.Errorf("CrossOver path or TurtleWoW path is not set. Cannot launch WoW."), myWindow) - return - } + mtlHudValue := "0" + if EnableMetalHud { + mtlHudValue = "1" + } - mtlHudValue := "0" - if EnableMetalHud { - mtlHudValue = "1" - } + // Prepare environment variables + envVars := fmt.Sprintf(`WINEDLLOVERRIDES="d3d9=n,b" MTL_HUD_ENABLED=%s`, mtlHudValue) + if CustomEnvVars != "" { + envVars = CustomEnvVars + " " + envVars + } - // Prepare environment variables - envVars := fmt.Sprintf(`WINEDLLOVERRIDES="d3d9=n,b" MTL_HUD_ENABLED=%s`, mtlHudValue) - if CustomEnvVars != "" { - envVars = CustomEnvVars + " " + envVars - } + shellCmd := fmt.Sprintf(`cd %s && %s %s %s %s`, + utils.QuotePathForShell(paths.TurtlewowPath), + envVars, + utils.QuotePathForShell(rosettaExecutable), + utils.QuotePathForShell(wineloader2Path), + utils.QuotePathForShell(wowExePath)) - shellCmd := fmt.Sprintf(`cd %s && %s %s %s %s`, - utils.QuotePathForShell(paths.TurtlewowPath), - envVars, - utils.QuotePathForShell(rosettaExecutable), - utils.QuotePathForShell(wineloader2Path), - utils.QuotePathForShell(wowExePath)) + escapedShellCmd := utils.EscapeStringForAppleScript(shellCmd) + cmd2Script := fmt.Sprintf("tell application \"Terminal\" to do script \"%s\"", escapedShellCmd) - escapedShellCmd := utils.EscapeStringForAppleScript(shellCmd) - cmd2Script := fmt.Sprintf("tell application \"Terminal\" to do script \"%s\"", escapedShellCmd) + log.Println("Executing WoW launch command via AppleScript...") + if !utils.RunOsascript(cmd2Script, myWindow) { + return + } - log.Println("Executing updated WoW launch command via AppleScript...") - if !utils.RunOsascript(cmd2Script, myWindow) { - return - } - - log.Println("Launch commands executed. Check the new terminal windows.") - dialog.ShowInformation("Launched", "World of Warcraft is starting. Enjoy.", myWindow) - } else { - log.Println("User cancelled WoW launch after rosetta x87 initiation.") - dialog.ShowInformation("Cancelled", "WoW launch was cancelled.", myWindow) - } - }, myWindow) + log.Println("Launch command executed. Check the new terminal window.") } diff --git a/pkg/paths/paths.go b/pkg/paths/paths.go index 3f32743..3905de7 100644 --- a/pkg/paths/paths.go +++ b/pkg/paths/paths.go @@ -17,10 +17,12 @@ import ( const DefaultCrossOverPath = "/Applications/CrossOver.app" var ( - CrossoverPath string - TurtlewowPath string - PatchesAppliedTurtleWoW = false - PatchesAppliedCrossOver = false + CrossoverPath string + TurtlewowPath string + PatchesAppliedTurtleWoW = false + PatchesAppliedCrossOver = false + RosettaX87ServiceRunning = false + ServiceStarting = false ) func SelectCrossOverPath(myWindow fyne.Window, crossoverPathLabel *widget.RichText, updateAllStatuses func()) { diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..1a6b4e9 --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,234 @@ +package service + +import ( + "fmt" + "log" + "os/exec" + "path/filepath" + "syscall" + "time" + + "turtlesilicon/pkg/paths" + "turtlesilicon/pkg/utils" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" +) + +var ( + ServiceRunning = false + serviceCmd *exec.Cmd + servicePID int +) + +// StartRosettaX87Service starts the RosettaX87 service with sudo privileges +func StartRosettaX87Service(myWindow fyne.Window, updateAllStatuses func()) { + log.Println("Starting RosettaX87 service...") + + if paths.TurtlewowPath == "" { + dialog.ShowError(fmt.Errorf("TurtleWoW path not set. Please set it first"), myWindow) + return + } + + rosettaX87Dir := filepath.Join(paths.TurtlewowPath, "rosettax87") + rosettaX87Exe := filepath.Join(rosettaX87Dir, "rosettax87") + + if !utils.PathExists(rosettaX87Exe) { + dialog.ShowError(fmt.Errorf("rosettax87 executable not found at %s. Please apply TurtleWoW patches first", rosettaX87Exe), myWindow) + return + } + + if ServiceRunning { + dialog.ShowInformation("Service Status", "RosettaX87 service is already running.", myWindow) + return + } + + // Show password dialog + passwordEntry := widget.NewPasswordEntry() + passwordEntry.SetPlaceHolder("Enter your sudo password") + passwordEntry.Resize(fyne.NewSize(300, passwordEntry.MinSize().Height)) + + // Create a container with proper sizing + passwordForm := widget.NewForm(widget.NewFormItem("Password:", passwordEntry)) + passwordContainer := container.NewVBox( + widget.NewLabel("Enter your sudo password to start the RosettaX87 service:"), + passwordForm, + ) + passwordContainer.Resize(fyne.NewSize(400, 100)) + + // Create the dialog variable so we can reference it in the callback + var passwordDialog dialog.Dialog + + // Define the confirm logic as a function so it can be reused + confirmFunc := func() { + password := passwordEntry.Text + if password == "" { + dialog.ShowError(fmt.Errorf("password cannot be empty"), myWindow) + return + } + + // Close the dialog + passwordDialog.Hide() + + // Set starting state + paths.ServiceStarting = true + fyne.Do(func() { + updateAllStatuses() + }) + + // Start the service in a goroutine + go func() { + err := startServiceWithPassword(rosettaX87Dir, rosettaX87Exe, password) + paths.ServiceStarting = false + if err != nil { + log.Printf("Failed to start RosettaX87 service: %v", err) + fyne.Do(func() { + dialog.ShowError(fmt.Errorf("failed to start RosettaX87 service: %v", err), myWindow) + }) + ServiceRunning = false + } else { + log.Println("RosettaX87 service started successfully") + ServiceRunning = true + } + fyne.Do(func() { + updateAllStatuses() + }) + }() + } + + // Add Enter key support to password entry + passwordEntry.OnSubmitted = func(text string) { + confirmFunc() + } + + passwordDialog = dialog.NewCustomConfirm("Sudo Password Required", "Start Service", "Cancel", + passwordContainer, + func(confirmed bool) { + if !confirmed { + log.Println("Service start cancelled by user") + return + } + confirmFunc() + }, myWindow) + + passwordDialog.Show() + + // Focus the password entry after showing the dialog + myWindow.Canvas().Focus(passwordEntry) +} + +// startServiceWithPassword starts the service using sudo with the provided password +func startServiceWithPassword(workingDir, executable, password string) error { + // Use sudo with the password + cmd := exec.Command("sudo", "-S", executable) + cmd.Dir = workingDir + + // Create a pipe to send the password + stdin, err := cmd.StdinPipe() + if err != nil { + return fmt.Errorf("failed to create stdin pipe: %v", err) + } + + // Start the command + err = cmd.Start() + if err != nil { + return fmt.Errorf("failed to start command: %v", err) + } + + // Send the password + _, err = stdin.Write([]byte(password + "\n")) + if err != nil { + cmd.Process.Kill() + return fmt.Errorf("failed to send password: %v", err) + } + stdin.Close() + + // Store the command and PID for later termination + serviceCmd = cmd + servicePID = cmd.Process.Pid + + // Wait a moment to see if the process starts successfully + time.Sleep(2 * time.Second) + + // Check if the process is still running + if cmd.ProcessState != nil && cmd.ProcessState.Exited() { + return fmt.Errorf("process exited prematurely with code: %d", cmd.ProcessState.ExitCode()) + } + + log.Printf("RosettaX87 service started with PID: %d", servicePID) + return nil +} + +// StopRosettaX87Service stops the running RosettaX87 service +func StopRosettaX87Service(myWindow fyne.Window, updateAllStatuses func()) { + log.Println("Stopping RosettaX87 service...") + + if !ServiceRunning { + dialog.ShowInformation("Service Status", "RosettaX87 service is not running.", myWindow) + return + } + + if serviceCmd != nil && serviceCmd.Process != nil { + // Send SIGTERM to gracefully stop the process + err := serviceCmd.Process.Signal(syscall.SIGTERM) + if err != nil { + log.Printf("Failed to send SIGTERM to process: %v", err) + // Try SIGKILL as fallback + err = serviceCmd.Process.Kill() + if err != nil { + log.Printf("Failed to kill process: %v", err) + dialog.ShowError(fmt.Errorf("failed to stop service: %v", err), myWindow) + return + } + } + + // Wait for the process to exit + go func() { + serviceCmd.Wait() + ServiceRunning = false + serviceCmd = nil + servicePID = 0 + log.Println("RosettaX87 service stopped") + fyne.Do(func() { + dialog.ShowInformation("Service Stopped", "RosettaX87 service has been stopped.", myWindow) + updateAllStatuses() + }) + }() + } else { + ServiceRunning = false + updateAllStatuses() + } +} + +// IsServiceRunning checks if the RosettaX87 service is currently running +func IsServiceRunning() bool { + if !ServiceRunning { + return false + } + + // Double-check by verifying the process is still alive + if serviceCmd != nil && serviceCmd.Process != nil { + // Check if process is still running + err := serviceCmd.Process.Signal(syscall.Signal(0)) + if err != nil { + // Process is not running + ServiceRunning = false + serviceCmd = nil + servicePID = 0 + return false + } + } + + return ServiceRunning +} + +// CleanupService ensures the service is stopped when the application exits +func CleanupService() { + if ServiceRunning && serviceCmd != nil && serviceCmd.Process != nil { + log.Println("Cleaning up RosettaX87 service on application exit...") + serviceCmd.Process.Kill() + ServiceRunning = false + } +} diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 5e62639..3babbe0 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -6,10 +6,12 @@ import ( "os" // Added import for os.ReadFile "path/filepath" "strings" + "time" "turtlesilicon/pkg/launcher" // Corrected import path "turtlesilicon/pkg/patching" // Corrected import path "turtlesilicon/pkg/paths" // Corrected import path + "turtlesilicon/pkg/service" // Added service import "turtlesilicon/pkg/utils" // Corrected import path "fyne.io/fyne/v2" @@ -24,13 +26,17 @@ var ( turtlewowPathLabel *widget.RichText turtlewowStatusLabel *widget.RichText crossoverStatusLabel *widget.RichText + serviceStatusLabel *widget.RichText launchButton *widget.Button patchTurtleWoWButton *widget.Button patchCrossOverButton *widget.Button unpatchTurtleWoWButton *widget.Button unpatchCrossOverButton *widget.Button + startServiceButton *widget.Button + stopServiceButton *widget.Button metalHudCheckbox *widget.Check envVarsEntry *widget.Entry + pulsingActive = false ) func UpdateAllStatuses() { @@ -131,12 +137,89 @@ func UpdateAllStatuses() { // Update Launch Button State if launchButton != nil { - if paths.PatchesAppliedTurtleWoW && paths.PatchesAppliedCrossOver && paths.TurtlewowPath != "" && paths.CrossoverPath != "" { + // Now requires service to be running as well + if paths.PatchesAppliedTurtleWoW && paths.PatchesAppliedCrossOver && + paths.TurtlewowPath != "" && paths.CrossoverPath != "" && service.IsServiceRunning() { launchButton.Enable() } else { launchButton.Disable() } } + + // Update Service Status + if paths.ServiceStarting { + // Show pulsing "Starting..." when service is starting + if serviceStatusLabel != nil { + if !pulsingActive { + pulsingActive = true + go startPulsingAnimation() + } + } + if startServiceButton != nil { + startServiceButton.Disable() + } + if stopServiceButton != nil { + stopServiceButton.Disable() + } + } else if service.IsServiceRunning() { + pulsingActive = false + paths.RosettaX87ServiceRunning = true + if serviceStatusLabel != nil { + serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Running", Style: widget.RichTextStyle{ColorName: theme.ColorNameSuccess}}} + serviceStatusLabel.Refresh() + } + if startServiceButton != nil { + startServiceButton.Disable() + } + if stopServiceButton != nil { + stopServiceButton.Enable() + } + } else { + pulsingActive = false + paths.RosettaX87ServiceRunning = false + if serviceStatusLabel != nil { + serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: "Stopped", Style: widget.RichTextStyle{ColorName: theme.ColorNameError}}} + serviceStatusLabel.Refresh() + } + if startServiceButton != nil { + if paths.TurtlewowPath != "" && paths.PatchesAppliedTurtleWoW { + startServiceButton.Enable() + } else { + startServiceButton.Disable() + } + } + if stopServiceButton != nil { + stopServiceButton.Disable() + } + } +} + +// startPulsingAnimation creates a pulsing effect for the "Starting..." text +func startPulsingAnimation() { + dots := 0 + for pulsingActive && paths.ServiceStarting { + var text string + switch dots % 4 { + case 0: + text = "Starting" + case 1: + text = "Starting." + case 2: + text = "Starting.." + case 3: + text = "Starting..." + } + + if serviceStatusLabel != nil { + fyne.DoAndWait(func() { + serviceStatusLabel.Segments = []widget.RichTextSegment{&widget.TextSegment{Text: text, Style: widget.RichTextStyle{ColorName: theme.ColorNamePrimary}}} + serviceStatusLabel.Refresh() + }) + } + + time.Sleep(500 * time.Millisecond) + dots++ + } } func CreateUI(myWindow fyne.Window) fyne.CanvasObject { @@ -153,6 +236,7 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject { turtlewowPathLabel = widget.NewRichText() turtlewowStatusLabel = widget.NewRichText() crossoverStatusLabel = widget.NewRichText() + serviceStatusLabel = widget.NewRichText() // Load the application logo logoResource, err := fyne.LoadResourceFromPath("Icon.png") @@ -212,6 +296,12 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject { unpatchCrossOverButton = widget.NewButton("Unpatch CrossOver", func() { patching.UnpatchCrossOver(myWindow, UpdateAllStatuses) }) + startServiceButton = widget.NewButton("Start Service", func() { + service.StartRosettaX87Service(myWindow, UpdateAllStatuses) + }) + stopServiceButton = widget.NewButton("Stop Service", func() { + service.StopRosettaX87Service(myWindow, UpdateAllStatuses) + }) launchButton = widget.NewButton("Launch Game", func() { launcher.LaunchGame(myWindow) }) @@ -235,11 +325,24 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject { container.NewGridWithColumns(4, widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, unpatchCrossOverButton, ), + container.NewGridWithColumns(4, + widget.NewLabel("RosettaX87 Service:"), serviceStatusLabel, startServiceButton, stopServiceButton, + ), widget.NewSeparator(), ) UpdateAllStatuses() // Initial UI state update + // Set up periodic status updates to keep service status in sync + go func() { + for { + time.Sleep(5 * time.Second) // Check every 5 seconds + fyne.DoAndWait(func() { + UpdateAllStatuses() + }) + } + }() + // Create GitHub link githubURL := "https://github.com/tairasu/TurtleSilicon" parsedURL, err := url.Parse(githubURL)