integrated rosettax87 as a service

This commit is contained in:
aomizu
2025-05-29 17:52:10 +09:00
parent 29d2e8e09c
commit 085a975fb9
7 changed files with 389 additions and 58 deletions

View File

@@ -2,5 +2,5 @@
Icon = "Icon.png" Icon = "Icon.png"
Name = "TurtleSilicon" Name = "TurtleSilicon"
ID = "com.tairasu.turtlesilicon" ID = "com.tairasu.turtlesilicon"
Version = "1.0.7" Version = "1.1.0"
Build = 12 Build = 13

View File

@@ -50,10 +50,14 @@ All credit for the core translation layer `winerosetta` and `rosettax87` goes to
* Click "Patch TurtleWoW". * Click "Patch TurtleWoW".
* Click "Patch CrossOver". * Click "Patch CrossOver".
* Status indicators will turn green once patching is successful for each. * Status indicators will turn green once patching is successful for each.
6. **Launch Game**: 6. **Start RosettaX87 Service**:
* Once both paths are set and both components are patched, the "Launch Game" button will become active. Click it. * Click "Start RosettaX87 Service" and enter your sudo password when prompted.
* Follow the on-screen prompts (you will need to enter your password in a new Terminal window for `rosettax87`). * This will run the RosettaX87 service in the background and is required for launching the game.
7. **Enjoy**: Experience a significantly smoother Turtle WoW on your Apple Silicon Mac! * 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 ### Method 2: Running from Source Code

10
main.go
View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"turtlesilicon/pkg/service"
"turtlesilicon/pkg/ui" "turtlesilicon/pkg/ui"
"turtlesilicon/pkg/utils" "turtlesilicon/pkg/utils"
@@ -14,7 +15,7 @@ import (
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
const appVersion = "1.0.7" const appVersion = "1.1.0"
func main() { func main() {
myApp := app.NewWithID("com.tairasu.turtlesilicon") myApp := app.NewWithID("com.tairasu.turtlesilicon")
@@ -53,5 +54,12 @@ func main() {
content := ui.CreateUI(myWindow) content := ui.CreateUI(myWindow)
myWindow.SetContent(content) 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() myWindow.ShowAndRun()
} }

View File

@@ -56,59 +56,39 @@ func LaunchGame(myWindow fyne.Window) {
return return
} }
appleScriptSafeRosettaDir := utils.EscapeStringForAppleScript(rosettaInTurtlePath) // Since RosettaX87 service is already running, we can directly launch WoW
cmd1Script := fmt.Sprintf("tell application \"Terminal\" to do script \"cd \" & quoted form of \"%s\" & \" && sudo ./rosettax87\"", appleScriptSafeRosettaDir) log.Println("RosettaX87 service is running. Proceeding to launch WoW.")
log.Println("Launching rosettax87 (requires sudo password in new terminal)...") if paths.CrossoverPath == "" || paths.TurtlewowPath == "" {
if !utils.RunOsascript(cmd1Script, myWindow) { dialog.ShowError(fmt.Errorf("CrossOver path or TurtleWoW path is not set. Cannot launch WoW."), myWindow)
return return
} }
dialog.ShowConfirm("Action Required", mtlHudValue := "0"
"The rosetta x87 terminal has been initiated.\n\n"+ if EnableMetalHud {
"1. Please enter your sudo password in that new terminal window.\n"+ mtlHudValue = "1"
"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" // Prepare environment variables
if EnableMetalHud { envVars := fmt.Sprintf(`WINEDLLOVERRIDES="d3d9=n,b" MTL_HUD_ENABLED=%s`, mtlHudValue)
mtlHudValue = "1" if CustomEnvVars != "" {
} envVars = CustomEnvVars + " " + envVars
}
// Prepare environment variables shellCmd := fmt.Sprintf(`cd %s && %s %s %s %s`,
envVars := fmt.Sprintf(`WINEDLLOVERRIDES="d3d9=n,b" MTL_HUD_ENABLED=%s`, mtlHudValue) utils.QuotePathForShell(paths.TurtlewowPath),
if CustomEnvVars != "" { envVars,
envVars = CustomEnvVars + " " + envVars utils.QuotePathForShell(rosettaExecutable),
} utils.QuotePathForShell(wineloader2Path),
utils.QuotePathForShell(wowExePath))
shellCmd := fmt.Sprintf(`cd %s && %s %s %s %s`, escapedShellCmd := utils.EscapeStringForAppleScript(shellCmd)
utils.QuotePathForShell(paths.TurtlewowPath), cmd2Script := fmt.Sprintf("tell application \"Terminal\" to do script \"%s\"", escapedShellCmd)
envVars,
utils.QuotePathForShell(rosettaExecutable),
utils.QuotePathForShell(wineloader2Path),
utils.QuotePathForShell(wowExePath))
escapedShellCmd := utils.EscapeStringForAppleScript(shellCmd) log.Println("Executing WoW launch command via AppleScript...")
cmd2Script := fmt.Sprintf("tell application \"Terminal\" to do script \"%s\"", escapedShellCmd) if !utils.RunOsascript(cmd2Script, myWindow) {
return
}
log.Println("Executing updated WoW launch command via AppleScript...") log.Println("Launch command executed. Check the new terminal window.")
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)
} }

View File

@@ -17,10 +17,12 @@ import (
const DefaultCrossOverPath = "/Applications/CrossOver.app" const DefaultCrossOverPath = "/Applications/CrossOver.app"
var ( var (
CrossoverPath string CrossoverPath string
TurtlewowPath string TurtlewowPath string
PatchesAppliedTurtleWoW = false PatchesAppliedTurtleWoW = false
PatchesAppliedCrossOver = false PatchesAppliedCrossOver = false
RosettaX87ServiceRunning = false
ServiceStarting = false
) )
func SelectCrossOverPath(myWindow fyne.Window, crossoverPathLabel *widget.RichText, updateAllStatuses func()) { func SelectCrossOverPath(myWindow fyne.Window, crossoverPathLabel *widget.RichText, updateAllStatuses func()) {

234
pkg/service/service.go Normal file
View File

@@ -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
}
}

View File

@@ -6,10 +6,12 @@ import (
"os" // Added import for os.ReadFile "os" // Added import for os.ReadFile
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"turtlesilicon/pkg/launcher" // Corrected import path "turtlesilicon/pkg/launcher" // Corrected import path
"turtlesilicon/pkg/patching" // Corrected import path "turtlesilicon/pkg/patching" // Corrected import path
"turtlesilicon/pkg/paths" // Corrected import path "turtlesilicon/pkg/paths" // Corrected import path
"turtlesilicon/pkg/service" // Added service import
"turtlesilicon/pkg/utils" // Corrected import path "turtlesilicon/pkg/utils" // Corrected import path
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@@ -24,13 +26,17 @@ var (
turtlewowPathLabel *widget.RichText turtlewowPathLabel *widget.RichText
turtlewowStatusLabel *widget.RichText turtlewowStatusLabel *widget.RichText
crossoverStatusLabel *widget.RichText crossoverStatusLabel *widget.RichText
serviceStatusLabel *widget.RichText
launchButton *widget.Button launchButton *widget.Button
patchTurtleWoWButton *widget.Button patchTurtleWoWButton *widget.Button
patchCrossOverButton *widget.Button patchCrossOverButton *widget.Button
unpatchTurtleWoWButton *widget.Button unpatchTurtleWoWButton *widget.Button
unpatchCrossOverButton *widget.Button unpatchCrossOverButton *widget.Button
startServiceButton *widget.Button
stopServiceButton *widget.Button
metalHudCheckbox *widget.Check metalHudCheckbox *widget.Check
envVarsEntry *widget.Entry envVarsEntry *widget.Entry
pulsingActive = false
) )
func UpdateAllStatuses() { func UpdateAllStatuses() {
@@ -131,12 +137,89 @@ func UpdateAllStatuses() {
// Update Launch Button State // Update Launch Button State
if launchButton != nil { 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() launchButton.Enable()
} else { } else {
launchButton.Disable() 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 { func CreateUI(myWindow fyne.Window) fyne.CanvasObject {
@@ -153,6 +236,7 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject {
turtlewowPathLabel = widget.NewRichText() turtlewowPathLabel = widget.NewRichText()
turtlewowStatusLabel = widget.NewRichText() turtlewowStatusLabel = widget.NewRichText()
crossoverStatusLabel = widget.NewRichText() crossoverStatusLabel = widget.NewRichText()
serviceStatusLabel = widget.NewRichText()
// Load the application logo // Load the application logo
logoResource, err := fyne.LoadResourceFromPath("Icon.png") logoResource, err := fyne.LoadResourceFromPath("Icon.png")
@@ -212,6 +296,12 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject {
unpatchCrossOverButton = widget.NewButton("Unpatch CrossOver", func() { unpatchCrossOverButton = widget.NewButton("Unpatch CrossOver", func() {
patching.UnpatchCrossOver(myWindow, UpdateAllStatuses) 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() { launchButton = widget.NewButton("Launch Game", func() {
launcher.LaunchGame(myWindow) launcher.LaunchGame(myWindow)
}) })
@@ -235,11 +325,24 @@ func CreateUI(myWindow fyne.Window) fyne.CanvasObject {
container.NewGridWithColumns(4, container.NewGridWithColumns(4,
widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, unpatchCrossOverButton, widget.NewLabel("CrossOver Patch:"), crossoverStatusLabel, patchCrossOverButton, unpatchCrossOverButton,
), ),
container.NewGridWithColumns(4,
widget.NewLabel("RosettaX87 Service:"), serviceStatusLabel, startServiceButton, stopServiceButton,
),
widget.NewSeparator(), widget.NewSeparator(),
) )
UpdateAllStatuses() // Initial UI state update 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 // Create GitHub link
githubURL := "https://github.com/tairasu/TurtleSilicon" githubURL := "https://github.com/tairasu/TurtleSilicon"
parsedURL, err := url.Parse(githubURL) parsedURL, err := url.Parse(githubURL)