Files
AUTO-MAS-test/Go_Updater/main.go
AoXuan 228e66315c feat(Go_Updater): 添加全新 Go 语言实现的自动更新器
- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等
- 实现了与 MirrorChyan API 交互的客户端逻辑
- 添加了版本检查、更新检测和下载 URL 生成等功能
- 嵌入了配置模板和资源文件系统
- 提供了完整的构建和发布流程
2025-07-20 16:30:14 +08:00

1046 lines
30 KiB
Go

package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"lightweight-updater/api"
"lightweight-updater/config"
"lightweight-updater/download"
"lightweight-updater/errors"
"lightweight-updater/install"
"lightweight-updater/logger"
appversion "lightweight-updater/version"
)
// UpdateState represents the current state of the update process
type UpdateState int
const (
StateIdle UpdateState = iota
StateChecking
StateUpdateAvailable
StateDownloading
StateInstalling
StateCompleted
StateError
)
// String returns the string representation of the update state
func (s UpdateState) String() string {
switch s {
case StateIdle:
return "Idle"
case StateChecking:
return "Checking"
case StateUpdateAvailable:
return "UpdateAvailable"
case StateDownloading:
return "Downloading"
case StateInstalling:
return "Installing"
case StateCompleted:
return "Completed"
case StateError:
return "Error"
default:
return "Unknown"
}
}
// GUIManager interface for optional GUI functionality
type GUIManager interface {
ShowMainWindow()
UpdateStatus(status int, message string)
ShowProgress(percentage float64)
ShowError(errorMsg string)
Close()
}
// UpdateInfo contains information about an available update
type UpdateInfo struct {
CurrentVersion string
NewVersion string
DownloadURL string
ReleaseNotes string
IsAvailable bool
}
// Application represents the main application instance
type Application struct {
config *config.Config
configManager config.ConfigManager
apiClient api.MirrorClient
downloadManager download.DownloadManager
installManager install.InstallManager
guiManager GUIManager
logger logger.Logger
errorHandler errors.ErrorHandler
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// Update flow state
currentState UpdateState
stateMutex sync.RWMutex
updateInfo *UpdateInfo
userConfirmed chan bool
}
// Command line flags
var (
configPath = flag.String("config", "", "Path to configuration file")
logLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error)")
noGUI = flag.Bool("no-gui", false, "Run without GUI (command line mode)")
version = flag.Bool("version", false, "Show version information")
help = flag.Bool("help", false, "Show help information")
channel = flag.String("channel", "", "Update channel (stable or beta)")
currentVersion = flag.String("current-version", "", "Current version to check against")
cdk = flag.String("cdk", "", "CDK for MirrorChyan download")
)
// Version information is now handled by the version package
func main() {
// Parse command line arguments
flag.Parse()
// Show version information
if *version {
showVersion()
return
}
// Show help information
if *help {
showHelp()
return
}
// Check for single instance
if err := ensureSingleInstance(); err != nil {
fmt.Fprintf(os.Stderr, "Another instance is already running: %v\n", err)
os.Exit(1)
}
// Initialize application
app, err := initializeApplication()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize application: %v\n", err)
os.Exit(1)
}
defer app.cleanup()
// Handle cleanup on process marked files on startup
if err := app.handleStartupCleanup(); err != nil {
app.logger.Warn("Failed to cleanup marked files: %v", err)
}
// Setup signal handling
app.setupSignalHandling()
// Start the application
if err := app.run(); err != nil {
app.logger.Error("Application error: %v", err)
os.Exit(1)
}
}
// initializeApplication initializes all application components
func initializeApplication() (*Application, error) {
// Create context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
// Initialize logger first
loggerConfig := logger.DefaultLoggerConfig()
// Set log level from command line
switch *logLevel {
case "debug":
loggerConfig.Level = logger.DEBUG
case "info":
loggerConfig.Level = logger.INFO
case "warn":
loggerConfig.Level = logger.WARN
case "error":
loggerConfig.Level = logger.ERROR
}
var appLogger logger.Logger
fileLogger, err := logger.NewFileLogger(loggerConfig)
if err != nil {
// Fallback to console logger
appLogger = logger.NewConsoleLogger(os.Stdout)
} else {
appLogger = fileLogger
}
appLogger.Info("Initializing AUTO_MAA_Go_Updater v%s", appversion.Version)
// Initialize configuration manager
var configManager config.ConfigManager
if *configPath != "" {
// Custom config path not implemented in the config package yet
// For now, use default manager
configManager = config.NewConfigManager()
appLogger.Warn("Custom config path not fully supported yet, using default")
} else {
configManager = config.NewConfigManager()
}
// Load configuration
cfg, err := configManager.Load()
if err != nil {
appLogger.Error("Failed to load configuration: %v", err)
return nil, fmt.Errorf("failed to load configuration: %w", err)
}
appLogger.Info("Configuration loaded successfully")
// Initialize API client
apiClient := api.NewClient()
// Initialize download manager
downloadManager := download.NewManager()
// Initialize install manager
installManager := install.NewManager()
// Initialize error handler
errorHandler := errors.NewDefaultErrorHandler()
// Initialize GUI manager (if not in no-gui mode)
var guiManager GUIManager
if !*noGUI {
// GUI will be implemented when GUI dependencies are available
appLogger.Info("GUI mode requested but not available in this build")
guiManager = nil
} else {
appLogger.Info("Running in no-GUI mode")
}
app := &Application{
config: cfg,
configManager: configManager,
apiClient: apiClient,
downloadManager: downloadManager,
installManager: installManager,
guiManager: guiManager,
logger: appLogger,
errorHandler: errorHandler,
ctx: ctx,
cancel: cancel,
currentState: StateIdle,
userConfirmed: make(chan bool, 1),
}
appLogger.Info("Application initialized successfully")
return app, nil
}
// run starts the main application logic
func (app *Application) run() error {
app.logger.Info("Starting application")
if app.guiManager != nil {
// Run with GUI
return app.runWithGUI()
} else {
// Run in command line mode
return app.runCommandLine()
}
}
// runWithGUI runs the application with GUI
func (app *Application) runWithGUI() error {
app.logger.Info("Starting GUI mode")
// Set up GUI callbacks
app.setupGUICallbacks()
// Show main window (this will block until window is closed)
app.guiManager.ShowMainWindow()
return nil
}
// runCommandLine runs the application in command line mode
func (app *Application) runCommandLine() error {
app.logger.Info("Starting command line mode")
// Start the complete update flow
return app.executeUpdateFlow()
}
// setupGUICallbacks sets up callbacks for GUI interactions
func (app *Application) setupGUICallbacks() {
if app.guiManager == nil {
return
}
// GUI callbacks will be implemented when GUI is available
app.logger.Info("GUI callbacks setup requested but GUI not available")
// For now, we'll set up basic interaction handling
// The actual GUI integration will be completed when GUI dependencies are resolved
}
// handleStartupCleanup handles cleanup of files marked for deletion on startup
func (app *Application) handleStartupCleanup() error {
app.logger.Info("Performing startup cleanup")
// Get current executable directory
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
exeDir := filepath.Dir(exePath)
// Delete files marked for deletion
if installMgr, ok := app.installManager.(*install.Manager); ok {
if err := installMgr.DeleteMarkedFiles(exeDir); err != nil {
return fmt.Errorf("failed to delete marked files: %w", err)
}
}
app.logger.Info("Startup cleanup completed")
return nil
}
// setupSignalHandling sets up graceful shutdown on system signals
func (app *Application) setupSignalHandling() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
app.logger.Info("Received signal: %v", sig)
app.logger.Info("Initiating graceful shutdown...")
app.cancel()
}()
}
// cleanup performs application cleanup
func (app *Application) cleanup() {
app.logger.Info("Cleaning up application resources")
// Cancel context to stop all operations
app.cancel()
// Wait for all goroutines to finish
app.wg.Wait()
// Cleanup install manager temporary directories
if installMgr, ok := app.installManager.(*install.Manager); ok {
if err := installMgr.CleanupAllTempDirs(); err != nil {
app.logger.Error("Failed to cleanup temp directories: %v", err)
}
}
app.logger.Info("Application cleanup completed")
// Close logger last
if err := app.logger.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to close logger: %v\n", err)
}
}
// ensureSingleInstance ensures only one instance of the application is running
func ensureSingleInstance() error {
// Create a lock file in temp directory
tempDir := os.TempDir()
lockFile := filepath.Join(tempDir, "lightweight-updater.lock")
// Try to create the lock file exclusively
file, err := os.OpenFile(lockFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
if os.IsExist(err) {
// Check if the process is still running
if isProcessRunning(lockFile) {
return fmt.Errorf("another instance is already running")
}
// Remove stale lock file and try again
os.Remove(lockFile)
return ensureSingleInstance()
}
return fmt.Errorf("failed to create lock file: %w", err)
}
// Write current process ID to lock file
fmt.Fprintf(file, "%d", os.Getpid())
file.Close()
// Remove lock file on exit
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
os.Remove(lockFile)
}()
return nil
}
// isProcessRunning checks if the process in the lock file is still running
func isProcessRunning(lockFile string) bool {
data, err := os.ReadFile(lockFile)
if err != nil {
return false
}
var pid int
if _, err := fmt.Sscanf(string(data), "%d", &pid); err != nil {
return false
}
// Check if process exists (Windows specific)
process, err := os.FindProcess(pid)
if err != nil {
return false
}
// On Windows, FindProcess always succeeds, so we need to check differently
// Try to send signal 0 to check if process exists
err = process.Signal(syscall.Signal(0))
return err == nil
}
// showVersion displays version information
func showVersion() {
fmt.Printf("AUTO_MAA_Go_Updater\n")
fmt.Printf("Version: %s\n", appversion.Version)
fmt.Printf("Build Time: %s\n", appversion.BuildTime)
fmt.Printf("Git Commit: %s\n", appversion.GitCommit)
}
// showHelp displays help information
func showHelp() {
fmt.Printf("AUTO_MAA_Go_Updater - AUTO_MAA 轻量级更新器\n\n")
fmt.Printf("Usage: %s [options]\n\n", os.Args[0])
fmt.Printf("Options:\n")
flag.PrintDefaults()
fmt.Printf("\nExamples:\n")
fmt.Printf(" %s # Run with GUI\n", os.Args[0])
fmt.Printf(" %s -no-gui # Run in command line mode\n", os.Args[0])
fmt.Printf(" %s -log-level debug # Run with debug logging\n", os.Args[0])
fmt.Printf(" %s -version # Show version information\n", os.Args[0])
}
// executeUpdateFlow executes the complete update flow with state machine management
func (app *Application) executeUpdateFlow() error {
app.logger.Info("Starting update flow execution")
// Execute the state machine
for {
select {
case <-app.ctx.Done():
app.logger.Info("Update flow cancelled")
return app.ctx.Err()
default:
}
// Get current state
state := app.getCurrentState()
app.logger.Debug("Current state: %s", state.String())
// Execute state logic
nextState, err := app.executeState(state)
if err != nil {
app.logger.Error("State execution failed: %v", err)
app.setState(StateError)
return err
}
// Check if we're done
if nextState == StateCompleted || nextState == StateError {
app.setState(nextState)
break
}
// Transition to next state
app.setState(nextState)
}
finalState := app.getCurrentState()
app.logger.Info("Update flow completed with state: %s", finalState.String())
if finalState == StateError {
return fmt.Errorf("update flow failed")
}
return nil
}
// executeState executes the logic for the current state and returns the next state
func (app *Application) executeState(state UpdateState) (UpdateState, error) {
switch state {
case StateIdle:
return app.executeIdleState()
case StateChecking:
return app.executeCheckingState()
case StateUpdateAvailable:
return app.executeUpdateAvailableState()
case StateDownloading:
return app.executeDownloadingState()
case StateInstalling:
return app.executeInstallingState()
case StateCompleted:
return StateCompleted, nil
case StateError:
return StateError, nil
default:
return StateError, fmt.Errorf("unknown state: %s", state.String())
}
}
// executeIdleState handles the idle state
func (app *Application) executeIdleState() (UpdateState, error) {
app.logger.Info("Starting update check...")
fmt.Println("正在检查更新...")
return StateChecking, nil
}
// executeCheckingState handles the checking state
func (app *Application) executeCheckingState() (UpdateState, error) {
app.logger.Info("Checking for updates")
// Determine version and channel to use
var currentVer, updateChannel, cdkToUse string
var err error
// Priority: command line args > version file > config
if *currentVersion != "" {
currentVer = *currentVersion
app.logger.Info("Using current version from command line: %s", currentVer)
} else {
// Try to load version from resources/version.json
versionManager := appversion.NewVersionManager()
versionInfo, err := versionManager.LoadVersionFromFile()
if err != nil {
app.logger.Warn("Failed to load version from file: %v, using config version", err)
currentVer = app.config.CurrentVersion
} else {
currentVer = versionInfo.MainVersion
app.logger.Info("Using current version from version file: %s", currentVer)
}
}
// Determine channel
if *channel != "" {
updateChannel = *channel
app.logger.Info("Using channel from command line: %s", updateChannel)
} else {
// Try to load channel from config.json
updateChannel = app.loadChannelFromConfig()
app.logger.Info("Using channel from config: %s", updateChannel)
}
// Determine CDK to use
if *cdk != "" {
cdkToUse = *cdk
app.logger.Info("Using CDK from command line")
} else {
// Get CDK from config
cdkToUse, err = app.config.GetCDK()
if err != nil {
app.logger.Warn("Failed to get CDK from config: %v", err)
cdkToUse = "" // Continue without CDK
}
}
// Prepare API parameters
params := api.UpdateCheckParams{
ResourceID: "AUTO_MAA", // Fixed resource ID for AUTO_MAA
CurrentVersion: currentVer,
Channel: updateChannel,
CDK: cdkToUse,
UserAgent: app.config.UserAgent,
}
// Call MirrorChyan API to check for updates
response, err := app.apiClient.CheckUpdate(params)
if err != nil {
app.logger.Error("Failed to check for updates: %v", err)
fmt.Printf("检查更新失败: %v\n", err)
return StateError, fmt.Errorf("failed to check for updates: %w", err)
}
// Check if update is available
isUpdateAvailable := app.apiClient.IsUpdateAvailable(response, currentVer)
if !isUpdateAvailable {
app.logger.Info("No update available")
fmt.Println("当前已是最新版本")
return StateCompleted, nil
}
// Determine download URL
var downloadURL string
if response.Data.URL != "" {
// Use CDK download URL from MirrorChyan
downloadURL = response.Data.URL
app.logger.Info("Using CDK download URL from MirrorChyan")
} else {
// Use official download site
downloadURL = app.apiClient.GetOfficialDownloadURL(response.Data.VersionName)
app.logger.Info("Using official download URL: %s", downloadURL)
}
// Store update information
app.updateInfo = &UpdateInfo{
CurrentVersion: currentVer,
NewVersion: response.Data.VersionName,
DownloadURL: downloadURL,
ReleaseNotes: response.Data.ReleaseNote,
IsAvailable: true,
}
app.logger.Info("Update available: %s -> %s", currentVer, response.Data.VersionName)
fmt.Printf("发现新版本: %s -> %s\n", currentVer, response.Data.VersionName)
// if response.Data.ReleaseNote != "" {
// fmt.Printf("更新内容: %s\n", response.Data.ReleaseNote)
// }
return StateUpdateAvailable, nil
}
// executeUpdateAvailableState handles the update available state
func (app *Application) executeUpdateAvailableState() (UpdateState, error) {
app.logger.Info("Update available, starting download automatically")
// Automatically start download without user confirmation
fmt.Println("开始下载更新...")
return StateDownloading, nil
}
// executeDownloadingState handles the downloading state
func (app *Application) executeDownloadingState() (UpdateState, error) {
app.logger.Info("Starting download")
if app.updateInfo == nil || app.updateInfo.DownloadURL == "" {
return StateError, fmt.Errorf("no download URL available")
}
// Get current executable directory
exePath, err := os.Executable()
if err != nil {
return StateError, fmt.Errorf("failed to get executable path: %w", err)
}
exeDir := filepath.Dir(exePath)
// Create AUTOMAA_UPDATE_TEMP directory for download
tempDir := filepath.Join(exeDir, "AUTOMAA_UPDATE_TEMP")
if err := os.MkdirAll(tempDir, 0755); err != nil {
return StateError, fmt.Errorf("failed to create temp directory: %w", err)
}
// Download file
downloadPath := filepath.Join(tempDir, "update.zip")
fmt.Println("正在下载更新包...")
// Create progress callback
progressCallback := func(progress download.DownloadProgress) {
if progress.TotalBytes > 0 {
fmt.Printf("\r下载进度: %.1f%% (%s/s)",
progress.Percentage,
app.formatBytes(progress.Speed))
}
}
// Download the update file
downloadErr := app.downloadManager.Download(app.updateInfo.DownloadURL, downloadPath, progressCallback)
fmt.Println() // New line after progress
if downloadErr != nil {
app.logger.Error("Download failed: %v", downloadErr)
fmt.Printf("下载失败: %v\n", downloadErr)
return StateError, fmt.Errorf("download failed: %w", downloadErr)
}
app.logger.Info("Download completed successfully")
fmt.Println("下载完成")
// Store download path for installation
app.updateInfo.DownloadURL = downloadPath
return StateInstalling, nil
}
// executeInstallingState handles the installing state
func (app *Application) executeInstallingState() (UpdateState, error) {
app.logger.Info("Starting installation")
fmt.Println("正在安装更新...")
if app.updateInfo == nil || app.updateInfo.DownloadURL == "" {
return StateError, fmt.Errorf("no download file available")
}
downloadPath := app.updateInfo.DownloadURL
// Create temporary directory for extraction
tempDir, err := app.installManager.CreateTempDir()
if err != nil {
return StateError, fmt.Errorf("failed to create temp directory: %w", err)
}
// Extract the downloaded zip file
app.logger.Info("Extracting update package")
if err := app.installManager.ExtractZip(downloadPath, tempDir); err != nil {
app.logger.Error("Failed to extract zip: %v", err)
return StateError, fmt.Errorf("failed to extract update package: %w", err)
}
// Process changes.json if it exists (for future use)
changesPath := filepath.Join(tempDir, "changes.json")
_, err = app.installManager.ProcessChanges(changesPath)
if err != nil {
app.logger.Warn("Failed to process changes (not critical): %v", err)
// This is not critical for AUTO_MAA-Setup.exe installation
}
// Get current executable directory
exePath, err := os.Executable()
if err != nil {
return StateError, fmt.Errorf("failed to get executable path: %w", err)
}
targetDir := filepath.Dir(exePath)
// Handle running processes (but skip the updater itself)
updaterName := filepath.Base(exePath)
if err := app.handleRunningProcesses(targetDir, updaterName); err != nil {
app.logger.Warn("Failed to handle running processes: %v", err)
// Continue with installation, this is not critical
}
// Look for AUTO_MAA-Setup.exe in the extracted files
setupExePath := filepath.Join(tempDir, "AUTO_MAA-Setup.exe")
if _, err := os.Stat(setupExePath); err != nil {
app.logger.Error("AUTO_MAA-Setup.exe not found in update package: %v", err)
return StateError, fmt.Errorf("AUTO_MAA-Setup.exe not found in update package: %w", err)
}
// Run the setup executable
app.logger.Info("Running AUTO_MAA-Setup.exe")
fmt.Println("正在运行安装程序...")
if err := app.runSetupExecutable(setupExePath); err != nil {
app.logger.Error("Failed to run setup executable: %v", err)
return StateError, fmt.Errorf("failed to run setup executable: %w", err)
}
// Update the version.json file with new version
if err := app.updateVersionFile(app.updateInfo.NewVersion); err != nil {
app.logger.Warn("Failed to update version file: %v", err)
// This is not critical, continue
}
// Clean up AUTOMAA_UPDATE_TEMP directory after installation
if err := os.RemoveAll(tempDir); err != nil {
app.logger.Warn("Failed to cleanup temp directory: %v", err)
// This is not critical, continue
} else {
app.logger.Info("Cleaned up temp directory: %s", tempDir)
}
app.logger.Info("Installation completed successfully")
fmt.Println("安装完成")
fmt.Printf("已更新到版本: %s\n", app.updateInfo.NewVersion)
return StateCompleted, nil
}
// getCurrentState returns the current state thread-safely
func (app *Application) getCurrentState() UpdateState {
app.stateMutex.RLock()
defer app.stateMutex.RUnlock()
return app.currentState
}
// setState sets the current state thread-safely
func (app *Application) setState(state UpdateState) {
app.stateMutex.Lock()
defer app.stateMutex.Unlock()
app.logger.Debug("State transition: %s -> %s", app.currentState.String(), state.String())
app.currentState = state
// Update GUI if available
if app.guiManager != nil {
app.updateGUIStatus(state)
}
}
// updateGUIStatus updates the GUI based on the current state
func (app *Application) updateGUIStatus(state UpdateState) {
if app.guiManager == nil {
return
}
switch state {
case StateIdle:
app.guiManager.UpdateStatus(0, "准备检查更新...")
case StateChecking:
app.guiManager.UpdateStatus(1, "正在检查更新...")
case StateUpdateAvailable:
if app.updateInfo != nil {
message := fmt.Sprintf("发现新版本: %s", app.updateInfo.NewVersion)
app.guiManager.UpdateStatus(2, message)
}
case StateDownloading:
app.guiManager.UpdateStatus(3, "正在下载更新...")
case StateInstalling:
app.guiManager.UpdateStatus(4, "正在安装更新...")
case StateCompleted:
app.guiManager.UpdateStatus(5, "更新完成")
case StateError:
app.guiManager.UpdateStatus(6, "更新失败")
}
}
// formatBytes formats bytes into human readable format
func (app *Application) formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// handleUserInteraction handles user interaction for GUI mode
func (app *Application) handleUserInteraction(action string) {
switch action {
case "confirm_update":
select {
case app.userConfirmed <- true:
default:
}
case "cancel_update":
select {
case app.userConfirmed <- false:
default:
}
case "check_update":
// Start update flow in a goroutine
app.wg.Add(1)
go func() {
defer app.wg.Done()
if err := app.executeUpdateFlow(); err != nil {
app.logger.Error("Update flow failed: %v", err)
}
}()
}
}
// updateVersionFile updates the target software's version.json file with the new version
func (app *Application) updateVersionFile(newVersion string) error {
// Get current executable directory (where the target software is located)
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
targetDir := filepath.Dir(exePath)
// Path to the target software's version file
versionFilePath := filepath.Join(targetDir, "resources", "version.json")
// Try to load existing version file
versionManager := appversion.NewVersionManager()
versionInfo, err := versionManager.LoadVersionFromFile()
if err != nil {
app.logger.Warn("Could not load existing version file, creating new one: %v", err)
// Create a basic version info structure
versionInfo = &appversion.VersionInfo{
MainVersion: newVersion,
VersionInfo: make(map[string]map[string][]string),
}
}
// Parse the new version to get the proper format
parsedVersion, err := appversion.ParseVersion(newVersion)
if err != nil {
// If we can't parse the version from API response, try to extract from display format
if strings.HasPrefix(newVersion, "v") {
// Convert "v4.4.1-beta3" to "4.4.1.3" format
versionStr := strings.TrimPrefix(newVersion, "v")
if strings.Contains(versionStr, "-beta") {
parts := strings.Split(versionStr, "-beta")
if len(parts) == 2 {
baseVersion := parts[0]
betaNum := parts[1]
versionInfo.MainVersion = fmt.Sprintf("%s.%s", baseVersion, betaNum)
} else {
versionInfo.MainVersion = versionStr + ".0"
}
} else {
versionInfo.MainVersion = versionStr + ".0"
}
} else {
versionInfo.MainVersion = newVersion
}
} else {
// Use the parsed version to create the proper format
versionInfo.MainVersion = parsedVersion.ToVersionString()
}
// Create resources directory if it doesn't exist
resourcesDir := filepath.Join(targetDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
return fmt.Errorf("failed to create resources directory: %w", err)
}
// Write updated version file
data, err := json.MarshalIndent(versionInfo, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal version info: %w", err)
}
if err := os.WriteFile(versionFilePath, data, 0644); err != nil {
return fmt.Errorf("failed to write version file: %w", err)
}
app.logger.Info("Updated version file: %s -> %s", versionFilePath, versionInfo.MainVersion)
return nil
}
// handleRunningProcesses handles running processes but excludes the updater itself
func (app *Application) handleRunningProcesses(targetDir, updaterName string) error {
app.logger.Info("Handling running processes, excluding updater: %s", updaterName)
// Get list of executable files in the target directory
files, err := os.ReadDir(targetDir)
if err != nil {
return fmt.Errorf("failed to read target directory: %w", err)
}
for _, file := range files {
if file.IsDir() {
continue
}
fileName := file.Name()
// Skip the updater itself
if fileName == updaterName {
app.logger.Info("Skipping updater file: %s", fileName)
continue
}
// Only handle .exe files
if !strings.HasSuffix(strings.ToLower(fileName), ".exe") {
continue
}
// Handle this executable
if err := app.installManager.HandleRunningProcess(fileName); err != nil {
app.logger.Warn("Failed to handle running process %s: %v", fileName, err)
// Continue with other files, don't fail the entire process
}
}
return nil
}
// runSetupExecutable runs the setup executable with proper parameters
func (app *Application) runSetupExecutable(setupExePath string) error {
app.logger.Info("Executing setup file: %s", setupExePath)
// Get current executable directory as installation directory
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
installDir := filepath.Dir(exePath)
// Setup command with parameters matching Python implementation
args := []string{
"/SP-", // Skip welcome page
"/SILENT", // Silent installation
"/NOCANCEL", // No cancel button
"/FORCECLOSEAPPLICATIONS", // Force close applications
"/LANG=Chinese", // Chinese language
fmt.Sprintf("/DIR=%s", installDir), // Installation directory
}
app.logger.Info("Running setup with args: %v", args)
// Create command with arguments
cmd := exec.Command(setupExePath, args...)
// Set working directory to the setup file's directory
cmd.Dir = filepath.Dir(setupExePath)
// Run the command and wait for it to complete
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to execute setup: %w", err)
}
app.logger.Info("Setup executable completed successfully")
return nil
}
// AutoMAAConfig represents the structure of config/config.json
type AutoMAAConfig struct {
Update struct {
UpdateType string `json:"UpdateType"`
} `json:"Update"`
}
// loadChannelFromConfig loads the update channel from config/config.json
func (app *Application) loadChannelFromConfig() string {
// Get current executable directory
exePath, err := os.Executable()
if err != nil {
app.logger.Warn("Failed to get executable path: %v", err)
return "stable"
}
configPath := filepath.Join(filepath.Dir(exePath), "config", "config.json")
// Check if config file exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
app.logger.Info("Config file not found: %s, using default channel", configPath)
return "stable"
}
// Read config file
data, err := os.ReadFile(configPath)
if err != nil {
app.logger.Warn("Failed to read config file: %v, using default channel", err)
return "stable"
}
// Parse JSON
var config AutoMAAConfig
if err := json.Unmarshal(data, &config); err != nil {
app.logger.Warn("Failed to parse config file: %v, using default channel", err)
return "stable"
}
// Get update channel
updateType := config.Update.UpdateType
if updateType == "" {
app.logger.Info("UpdateType not found in config, using default channel")
return "stable"
}
app.logger.Info("Loaded update channel from config: %s", updateType)
return updateType
}