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

@@ -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.")
}

View File

@@ -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()) {

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
"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)