integrated an auto-updater
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.DS_Store
|
||||
TurtleSilicon.*
|
||||
TurtleSilicon.app/Contents/MacOS/turtlesilicon
|
||||
*.dmg
|
||||
*.dmg
|
||||
turtlesilicon
|
@@ -3,4 +3,4 @@
|
||||
Name = "TurtleSilicon"
|
||||
ID = "com.tairasu.turtlesilicon"
|
||||
Version = "1.2.1"
|
||||
Build = 37
|
||||
Build = 47
|
||||
|
42
main.go
42
main.go
@@ -10,9 +10,6 @@ import (
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
const appVersion = "1.2.1"
|
||||
@@ -26,29 +23,28 @@ func main() {
|
||||
// Check for updates
|
||||
go func() {
|
||||
prefs, _ := utils.LoadPrefs()
|
||||
latest, notes, update, err := utils.CheckForUpdate(appVersion)
|
||||
debug.Printf("DEBUG RAW: latest=%q", latest)
|
||||
latestVersion := strings.TrimLeft(latest, "v.")
|
||||
debug.Printf("DEBUG: appVersion=%q, latest=%q, latestVersion=%q, suppressed=%q, update=%v, err=%v\n",
|
||||
appVersion, latest, latestVersion, prefs.SuppressedUpdateVersion, update, err)
|
||||
// Always skip popup if versions match
|
||||
if latestVersion == appVersion {
|
||||
updateInfo, updateAvailable, err := utils.CheckForUpdateWithAssets(appVersion)
|
||||
if err != nil {
|
||||
debug.Printf("Failed to check for updates: %v", err)
|
||||
return
|
||||
}
|
||||
if err == nil && update && prefs.SuppressedUpdateVersion != latestVersion {
|
||||
checkbox := widget.NewCheck("Do not show this anymore", func(bool) {})
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel("A new version ("+latestVersion+") is available!"),
|
||||
widget.NewLabel("Release notes:\n\n"+notes),
|
||||
checkbox,
|
||||
)
|
||||
dialog.ShowCustomConfirm("Update Available", "OK", "Cancel", content, func(ok bool) {
|
||||
if checkbox.Checked {
|
||||
prefs.SuppressedUpdateVersion = latestVersion
|
||||
utils.SavePrefs(prefs)
|
||||
}
|
||||
}, TSWindow)
|
||||
|
||||
if !updateAvailable {
|
||||
debug.Printf("No updates available")
|
||||
return
|
||||
}
|
||||
|
||||
latestVersion := strings.TrimPrefix(updateInfo.TagName, "v")
|
||||
debug.Printf("Update available: current=%s, latest=%s", appVersion, latestVersion)
|
||||
|
||||
// Skip if user has suppressed this version
|
||||
if prefs.SuppressedUpdateVersion == latestVersion {
|
||||
debug.Printf("Update suppressed by user: %s", latestVersion)
|
||||
return
|
||||
}
|
||||
|
||||
// Show enhanced update dialog
|
||||
ui.ShowUpdateDialog(updateInfo, appVersion, TSWindow)
|
||||
}()
|
||||
|
||||
content := ui.CreateUI(TSWindow)
|
||||
|
187
pkg/ui/updater.go
Normal file
187
pkg/ui/updater.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"turtlesilicon/pkg/debug"
|
||||
"turtlesilicon/pkg/utils"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// ShowUpdateDialog displays an enhanced update dialog with download and install options
|
||||
func ShowUpdateDialog(updateInfo *utils.UpdateInfo, currentVersion string, myWindow fyne.Window) {
|
||||
latestVersion := strings.TrimPrefix(updateInfo.TagName, "v")
|
||||
|
||||
// Find the DMG asset
|
||||
var dmgAsset *utils.Asset
|
||||
for _, asset := range updateInfo.Assets {
|
||||
if strings.HasSuffix(asset.Name, ".dmg") {
|
||||
dmgAsset = &asset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dmgAsset == nil {
|
||||
dialog.ShowError(fmt.Errorf("no DMG file found in the latest release"), myWindow)
|
||||
return
|
||||
}
|
||||
|
||||
// Create content for the update dialog
|
||||
titleLabel := widget.NewLabel(fmt.Sprintf("Update Available: v%s", latestVersion))
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
currentVersionLabel := widget.NewLabel(fmt.Sprintf("Current version: v%s", currentVersion))
|
||||
|
||||
// Format file size
|
||||
fileSize := formatFileSize(dmgAsset.Size)
|
||||
fileSizeLabel := widget.NewLabel(fmt.Sprintf("Download size: %s", fileSize))
|
||||
|
||||
// Release notes
|
||||
notesLabel := widget.NewLabel("Release notes:")
|
||||
notesLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
notesText := widget.NewRichTextFromMarkdown(updateInfo.Body)
|
||||
notesScroll := container.NewScroll(notesText)
|
||||
notesScroll.SetMinSize(fyne.NewSize(480, 120))
|
||||
|
||||
// Progress bar (initially hidden)
|
||||
progressBar := widget.NewProgressBar()
|
||||
progressBar.Hide()
|
||||
|
||||
progressLabel := widget.NewLabel("")
|
||||
progressLabel.Hide()
|
||||
|
||||
// Checkbox for suppressing this version
|
||||
suppressCheck := widget.NewCheck("Don't show this update again", nil)
|
||||
|
||||
content := container.NewVBox(
|
||||
titleLabel,
|
||||
currentVersionLabel,
|
||||
fileSizeLabel,
|
||||
widget.NewSeparator(),
|
||||
notesLabel,
|
||||
notesScroll,
|
||||
widget.NewSeparator(),
|
||||
progressBar,
|
||||
progressLabel,
|
||||
suppressCheck,
|
||||
)
|
||||
|
||||
// Create custom dialog
|
||||
d := dialog.NewCustom("New Update Available", "", content, myWindow)
|
||||
d.Resize(fyne.NewSize(550, 400))
|
||||
|
||||
// Download and install function
|
||||
downloadAndInstall := func() {
|
||||
// Show progress elements
|
||||
progressBar.Show()
|
||||
progressLabel.Show()
|
||||
progressLabel.SetText("Starting download...")
|
||||
|
||||
// Disable dialog closing during download
|
||||
d.SetButtons([]fyne.CanvasObject{})
|
||||
|
||||
go func() {
|
||||
// Download with progress
|
||||
downloadPath, err := utils.DownloadUpdate(dmgAsset.BrowserDownloadURL, func(downloaded, total int64) {
|
||||
// Update progress on UI thread
|
||||
fyne.NewAnimation(
|
||||
time.Millisecond*50,
|
||||
func(float32) {
|
||||
if total > 0 {
|
||||
progress := float64(downloaded) / float64(total)
|
||||
progressBar.SetValue(progress)
|
||||
progressLabel.SetText(fmt.Sprintf("Downloaded: %s / %s (%.1f%%)",
|
||||
formatFileSize(downloaded), formatFileSize(total), progress*100))
|
||||
}
|
||||
},
|
||||
).Curve = fyne.AnimationLinear
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
progressLabel.SetText(fmt.Sprintf("Download failed: %v", err))
|
||||
debug.Printf("Download failed: %v", err)
|
||||
|
||||
// Re-enable close button
|
||||
d.SetButtons([]fyne.CanvasObject{
|
||||
widget.NewButton("Close", func() { d.Hide() }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
progressLabel.SetText("Installing update...")
|
||||
progressBar.SetValue(1.0)
|
||||
|
||||
// Install update
|
||||
err = utils.InstallUpdate(downloadPath)
|
||||
if err != nil {
|
||||
progressLabel.SetText(fmt.Sprintf("Installation failed: %v", err))
|
||||
debug.Printf("Installation failed: %v", err)
|
||||
|
||||
// Re-enable close button
|
||||
d.SetButtons([]fyne.CanvasObject{
|
||||
widget.NewButton("Close", func() { d.Hide() }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Success - show restart dialog
|
||||
progressLabel.SetText("Update installed successfully!")
|
||||
|
||||
restartDialog := dialog.NewConfirm(
|
||||
"Update Complete",
|
||||
"The update has been installed successfully and will require a restart. Would you like to close the application now?",
|
||||
func(restart bool) {
|
||||
d.Hide()
|
||||
if restart {
|
||||
utils.RestartApp()
|
||||
fyne.CurrentApp().Quit()
|
||||
}
|
||||
},
|
||||
myWindow,
|
||||
)
|
||||
restartDialog.Show()
|
||||
}()
|
||||
}
|
||||
|
||||
// Set dialog buttons
|
||||
d.SetButtons([]fyne.CanvasObject{
|
||||
widget.NewButton("Download & Install", downloadAndInstall),
|
||||
widget.NewButton("Later", func() {
|
||||
if suppressCheck.Checked {
|
||||
// Save suppressed version
|
||||
prefs, _ := utils.LoadPrefs()
|
||||
prefs.SuppressedUpdateVersion = latestVersion
|
||||
utils.SavePrefs(prefs)
|
||||
}
|
||||
d.Hide()
|
||||
}),
|
||||
})
|
||||
|
||||
d.Show()
|
||||
}
|
||||
|
||||
// formatFileSize formats a file size in bytes to a human-readable string
|
||||
func formatFileSize(bytes int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = KB * 1024
|
||||
GB = MB * 1024
|
||||
)
|
||||
|
||||
switch {
|
||||
case bytes >= GB:
|
||||
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(GB))
|
||||
case bytes >= MB:
|
||||
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(MB))
|
||||
case bytes >= KB:
|
||||
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(KB))
|
||||
default:
|
||||
return fmt.Sprintf("%d bytes", bytes)
|
||||
}
|
||||
}
|
@@ -17,6 +17,14 @@ import (
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
)
|
||||
|
||||
// min returns the smaller of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// PathExists checks if a path exists.
|
||||
func PathExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
@@ -140,36 +148,430 @@ func CheckForUpdate(currentVersion string) (latestVersion, releaseNotes string,
|
||||
return latest, data.Body, latest != currentVersion, nil
|
||||
}
|
||||
|
||||
// GetBundledResourceSize returns the size of a bundled resource
|
||||
func GetBundledResourceSize(resourcePath string) (int64, error) {
|
||||
resource, err := fyne.LoadResourceFromPath(resourcePath)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to load bundled resource %s: %v", resourcePath, err)
|
||||
}
|
||||
return int64(len(resource.Content())), nil
|
||||
// UpdateInfo contains information about the latest release
|
||||
type UpdateInfo struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Body string `json:"body"`
|
||||
Assets []Asset `json:"assets"`
|
||||
}
|
||||
|
||||
// CompareFileWithBundledResource compares the size of a file with a bundled resource
|
||||
func CompareFileWithBundledResource(filePath, resourcePath string) bool {
|
||||
type Asset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// CheckForUpdateWithAssets returns update information including download assets
|
||||
func CheckForUpdateWithAssets(currentVersion string) (*UpdateInfo, bool, error) {
|
||||
resp, err := http.Get("https://api.github.com/repos/tairasu/TurtleSilicon/releases/latest")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check for HTTP errors
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, false, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Read the response body first to check content
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// Check if response looks like HTML (rate limiting or other errors)
|
||||
bodyStr := string(body)
|
||||
if strings.Contains(bodyStr, "<!DOCTYPE html>") || strings.Contains(bodyStr, "<html") {
|
||||
return nil, false, fmt.Errorf("GitHub API returned HTML instead of JSON (possible rate limiting): %s", bodyStr[:min(200, len(bodyStr))])
|
||||
}
|
||||
|
||||
var updateInfo UpdateInfo
|
||||
if err := json.Unmarshal(body, &updateInfo); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to parse JSON response: %v. Response: %s", err, bodyStr[:min(200, len(bodyStr))])
|
||||
}
|
||||
|
||||
latest := strings.TrimPrefix(updateInfo.TagName, "v")
|
||||
updateAvailable := latest != currentVersion
|
||||
|
||||
return &updateInfo, updateAvailable, nil
|
||||
}
|
||||
|
||||
// DownloadUpdate downloads the latest release and returns the path to the downloaded file
|
||||
func DownloadUpdate(downloadURL string, progressCallback func(downloaded, total int64)) (string, error) {
|
||||
// Create temporary file
|
||||
tempFile, err := os.CreateTemp("", "TurtleSilicon-update-*.dmg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Download the file
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
return "", fmt.Errorf("failed to download update: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
totalSize := resp.ContentLength
|
||||
var downloaded int64
|
||||
|
||||
// Copy with progress tracking
|
||||
buffer := make([]byte, 32*1024) // 32KB buffer
|
||||
for {
|
||||
n, err := resp.Body.Read(buffer)
|
||||
if n > 0 {
|
||||
_, writeErr := tempFile.Write(buffer[:n])
|
||||
if writeErr != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
return "", fmt.Errorf("failed to write update file: %v", writeErr)
|
||||
}
|
||||
downloaded += int64(n)
|
||||
if progressCallback != nil {
|
||||
progressCallback(downloaded, totalSize)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
return "", fmt.Errorf("failed to read update data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return tempFile.Name(), nil
|
||||
}
|
||||
|
||||
// InstallUpdate installs the downloaded update by mounting the DMG and replacing the current app
|
||||
func InstallUpdate(dmgPath string) error {
|
||||
// Get current app path
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %v", err)
|
||||
}
|
||||
|
||||
// Navigate up to find the .app bundle
|
||||
currentAppPath := execPath
|
||||
for !strings.HasSuffix(currentAppPath, ".app") && currentAppPath != "/" {
|
||||
currentAppPath = filepath.Dir(currentAppPath)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(currentAppPath, ".app") {
|
||||
return fmt.Errorf("could not find app bundle path")
|
||||
}
|
||||
|
||||
// Mount the DMG and parse the mount point from plist output
|
||||
debug.Printf("Mounting DMG: %s", dmgPath)
|
||||
mountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse", "-plist")
|
||||
mountOutput, err := mountCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount DMG: %v, output: %s", err, string(mountOutput))
|
||||
}
|
||||
|
||||
debug.Printf("Mount output: %s", string(mountOutput))
|
||||
|
||||
mountPoint := ""
|
||||
|
||||
// Parse the plist XML output to find mount points
|
||||
outputStr := string(mountOutput)
|
||||
|
||||
// Look for mount-point entries in the XML
|
||||
// The plist contains mount-point entries that show where volumes are mounted
|
||||
lines := strings.Split(outputStr, "\n")
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.Contains(line, "<key>mount-point</key>") && i+1 < len(lines) {
|
||||
// The next line should contain the mount point path
|
||||
nextLine := strings.TrimSpace(lines[i+1])
|
||||
if strings.HasPrefix(nextLine, "<string>/Volumes/") {
|
||||
// Extract the path from <string>/Volumes/...</string>
|
||||
start := strings.Index(nextLine, "<string>") + 8
|
||||
end := strings.Index(nextLine, "</string>")
|
||||
if start >= 8 && end > start {
|
||||
mountPoint = nextLine[start:end]
|
||||
debug.Printf("Found mount point in plist: %s", mountPoint)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try without -plist flag for simpler output
|
||||
if mountPoint == "" {
|
||||
debug.Printf("Plist parsing failed, trying simple mount")
|
||||
// Unmount first if something was mounted
|
||||
exec.Command("hdiutil", "detach", dmgPath, "-force").Run()
|
||||
|
||||
// Try mounting without plist
|
||||
simpleMountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse")
|
||||
simpleOutput, simpleErr := simpleMountCmd.CombinedOutput()
|
||||
if simpleErr != nil {
|
||||
return fmt.Errorf("failed to mount DMG (simple): %v, output: %s", simpleErr, string(simpleOutput))
|
||||
}
|
||||
|
||||
// Parse simple output
|
||||
simpleLines := strings.Split(string(simpleOutput), "\n")
|
||||
for _, line := range simpleLines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.Contains(line, "/Volumes/") {
|
||||
parts := strings.Fields(line)
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if strings.HasPrefix(parts[i], "/Volumes/") {
|
||||
mountPoint = parts[i]
|
||||
debug.Printf("Found mount point in simple output: %s", mountPoint)
|
||||
break
|
||||
}
|
||||
}
|
||||
if mountPoint != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mountPoint == "" {
|
||||
return fmt.Errorf("could not find mount point. Mount output: %s", string(mountOutput))
|
||||
}
|
||||
|
||||
debug.Printf("Using mount point: %s", mountPoint)
|
||||
|
||||
defer func() {
|
||||
// Unmount the DMG
|
||||
debug.Printf("Unmounting DMG from: %s", mountPoint)
|
||||
unmountCmd := exec.Command("hdiutil", "detach", mountPoint, "-force")
|
||||
unmountCmd.Run()
|
||||
}()
|
||||
|
||||
// Find the app in the mounted DMG - search for any .app bundle
|
||||
var newAppPath string
|
||||
|
||||
// First, try the exact name
|
||||
exactPath := filepath.Join(mountPoint, "TurtleSilicon.app")
|
||||
if PathExists(exactPath) {
|
||||
newAppPath = exactPath
|
||||
} else {
|
||||
// Search for any .app bundle in the mount point
|
||||
debug.Printf("TurtleSilicon.app not found at exact path, searching for .app bundles")
|
||||
entries, err := os.ReadDir(mountPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read DMG contents: %v", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && strings.HasSuffix(entry.Name(), ".app") {
|
||||
candidatePath := filepath.Join(mountPoint, entry.Name())
|
||||
debug.Printf("Found .app bundle: %s", candidatePath)
|
||||
newAppPath = candidatePath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if newAppPath == "" {
|
||||
return fmt.Errorf("no .app bundle found in DMG at %s", mountPoint)
|
||||
}
|
||||
|
||||
debug.Printf("Found app to install: %s", newAppPath)
|
||||
|
||||
// Create backup of current app
|
||||
backupPath := currentAppPath + ".backup"
|
||||
debug.Printf("Creating backup: %s -> %s", currentAppPath, backupPath)
|
||||
|
||||
// Remove old backup if it exists
|
||||
if PathExists(backupPath) {
|
||||
os.RemoveAll(backupPath)
|
||||
}
|
||||
|
||||
if err := CopyDir(currentAppPath, backupPath); err != nil {
|
||||
return fmt.Errorf("failed to create backup: %v", err)
|
||||
}
|
||||
|
||||
// Remove current app
|
||||
debug.Printf("Removing current app: %s", currentAppPath)
|
||||
if err := os.RemoveAll(currentAppPath); err != nil {
|
||||
return fmt.Errorf("failed to remove current app: %v", err)
|
||||
}
|
||||
|
||||
// Copy new app
|
||||
debug.Printf("Installing new app: %s -> %s", newAppPath, currentAppPath)
|
||||
if err := CopyDir(newAppPath, currentAppPath); err != nil {
|
||||
// Try to restore backup on failure
|
||||
debug.Printf("Installation failed, restoring backup")
|
||||
os.RemoveAll(currentAppPath)
|
||||
CopyDir(backupPath, currentAppPath)
|
||||
return fmt.Errorf("failed to install new app: %v", err)
|
||||
}
|
||||
|
||||
// Fix executable permissions for the main binary
|
||||
executablePath := filepath.Join(currentAppPath, "Contents", "MacOS", "turtlesilicon")
|
||||
if PathExists(executablePath) {
|
||||
debug.Printf("Setting executable permissions for: %s", executablePath)
|
||||
if err := os.Chmod(executablePath, 0755); err != nil {
|
||||
debug.Printf("Warning: failed to set executable permissions: %v", err)
|
||||
// Don't fail the entire update for this, but log it
|
||||
}
|
||||
} else {
|
||||
debug.Printf("Warning: executable not found at expected path: %s", executablePath)
|
||||
}
|
||||
|
||||
// Remove backup on success
|
||||
os.RemoveAll(backupPath)
|
||||
|
||||
debug.Printf("Update installed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestDMGMount tests DMG mounting and returns mount point and app path for debugging
|
||||
func TestDMGMount(dmgPath string) (string, string, error) {
|
||||
debug.Printf("Testing DMG mount: %s", dmgPath)
|
||||
|
||||
// Mount the DMG with verbose output to better parse mount point
|
||||
mountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse", "-plist")
|
||||
mountOutput, err := mountCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to mount DMG: %v, output: %s", err, string(mountOutput))
|
||||
}
|
||||
|
||||
debug.Printf("Mount output: %s", string(mountOutput))
|
||||
|
||||
// Parse mount output to get mount point
|
||||
mountPoint := ""
|
||||
|
||||
// First try: look for /Volumes/ in the output lines
|
||||
lines := strings.Split(string(mountOutput), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.Contains(line, "/Volumes/") {
|
||||
parts := strings.Fields(line)
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if strings.HasPrefix(parts[i], "/Volumes/") {
|
||||
mountPoint = parts[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if mountPoint != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second try: use hdiutil info to get mount points if first method failed
|
||||
if mountPoint == "" {
|
||||
debug.Printf("First mount point detection failed, trying hdiutil info")
|
||||
infoCmd := exec.Command("hdiutil", "info", "-plist")
|
||||
infoOutput, infoErr := infoCmd.CombinedOutput()
|
||||
if infoErr == nil {
|
||||
infoLines := strings.Split(string(infoOutput), "\n")
|
||||
for _, line := range infoLines {
|
||||
if strings.Contains(line, "/Volumes/") && strings.Contains(line, "TurtleSilicon") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "/Volumes/") {
|
||||
mountPoint = line
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mountPoint == "" {
|
||||
return "", "", fmt.Errorf("could not find mount point. Mount output: %s", string(mountOutput))
|
||||
}
|
||||
|
||||
debug.Printf("Using mount point: %s", mountPoint)
|
||||
|
||||
// Find the app in the mounted DMG
|
||||
var newAppPath string
|
||||
|
||||
// First, try the exact name
|
||||
exactPath := filepath.Join(mountPoint, "TurtleSilicon.app")
|
||||
if PathExists(exactPath) {
|
||||
newAppPath = exactPath
|
||||
} else {
|
||||
// Search for any .app bundle in the mount point
|
||||
debug.Printf("TurtleSilicon.app not found at exact path, searching for .app bundles")
|
||||
entries, err := os.ReadDir(mountPoint)
|
||||
if err != nil {
|
||||
// Unmount before returning error
|
||||
exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
|
||||
return "", "", fmt.Errorf("failed to read DMG contents: %v", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() && strings.HasSuffix(entry.Name(), ".app") {
|
||||
candidatePath := filepath.Join(mountPoint, entry.Name())
|
||||
debug.Printf("Found .app bundle: %s", candidatePath)
|
||||
newAppPath = candidatePath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unmount after testing
|
||||
debug.Printf("Unmounting test DMG from: %s", mountPoint)
|
||||
exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
|
||||
|
||||
if newAppPath == "" {
|
||||
return mountPoint, "", fmt.Errorf("no .app bundle found in DMG at %s", mountPoint)
|
||||
}
|
||||
|
||||
return mountPoint, newAppPath, nil
|
||||
}
|
||||
|
||||
// CompareFileWithBundledResource compares the size of a file on disk with a bundled resource
|
||||
func CompareFileWithBundledResource(filePath, resourceName string) bool {
|
||||
// Check if the file exists first
|
||||
if !PathExists(filePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get file size
|
||||
// Get file info for the existing file
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
debug.Printf("Failed to get file info for %s: %v", filePath, err)
|
||||
debug.Printf("Failed to stat file %s: %v", filePath, err)
|
||||
return false
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
|
||||
// Get bundled resource size
|
||||
resourceSize, err := GetBundledResourceSize(resourcePath)
|
||||
// Load the bundled resource
|
||||
resource, err := fyne.LoadResourceFromPath(resourceName)
|
||||
if err != nil {
|
||||
debug.Printf("Failed to get bundled resource size for %s: %v", resourcePath, err)
|
||||
debug.Printf("Failed to load bundled resource %s: %v", resourceName, err)
|
||||
return false
|
||||
}
|
||||
|
||||
debug.Printf("Comparing file sizes: %s (%d bytes) vs %s (%d bytes)", filePath, fileSize, resourcePath, resourceSize)
|
||||
// Compare sizes
|
||||
fileSize := fileInfo.Size()
|
||||
resourceSize := int64(len(resource.Content()))
|
||||
|
||||
debug.Printf("Comparing file sizes - %s: %d bytes vs bundled %s: %d bytes",
|
||||
filePath, fileSize, resourceName, resourceSize)
|
||||
|
||||
return fileSize == resourceSize
|
||||
}
|
||||
|
||||
// RestartApp restarts the application
|
||||
func RestartApp() error {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %v", err)
|
||||
}
|
||||
|
||||
// Find the .app bundle
|
||||
appPath := execPath
|
||||
for !strings.HasSuffix(appPath, ".app") && appPath != "/" {
|
||||
appPath = filepath.Dir(appPath)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(appPath, ".app") {
|
||||
// Launch the app bundle
|
||||
cmd := exec.Command("open", appPath)
|
||||
return cmd.Start()
|
||||
} else {
|
||||
// Launch the executable directly
|
||||
cmd := exec.Command(execPath)
|
||||
return cmd.Start()
|
||||
}
|
||||
}
|
||||
|
BIN
turtlesilicon
BIN
turtlesilicon
Binary file not shown.
Reference in New Issue
Block a user