feat(Go_Updater): 添加全新 Go 语言实现的自动更新器
- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等 - 实现了与 MirrorChyan API 交互的客户端逻辑 - 添加了版本检查、更新检测和下载 URL 生成等功能 - 嵌入了配置模板和资源文件系统 - 提供了完整的构建和发布流程
This commit is contained in:
219
Go_Updater/errors/errors.go
Normal file
219
Go_Updater/errors/errors.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrorType 定义错误类型枚举
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
NetworkError ErrorType = iota
|
||||
APIError
|
||||
FileError
|
||||
ConfigError
|
||||
InstallError
|
||||
)
|
||||
|
||||
// String 返回错误类型的字符串表示
|
||||
func (et ErrorType) String() string {
|
||||
switch et {
|
||||
case NetworkError:
|
||||
return "NetworkError"
|
||||
case APIError:
|
||||
return "APIError"
|
||||
case FileError:
|
||||
return "FileError"
|
||||
case ConfigError:
|
||||
return "ConfigError"
|
||||
case InstallError:
|
||||
return "InstallError"
|
||||
default:
|
||||
return "UnknownError"
|
||||
}
|
||||
}
|
||||
|
||||
// UpdaterError 统一的错误结构体
|
||||
type UpdaterError struct {
|
||||
Type ErrorType
|
||||
Message string
|
||||
Cause error
|
||||
Timestamp time.Time
|
||||
Context map[string]interface{}
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (ue *UpdaterError) Error() string {
|
||||
if ue.Cause != nil {
|
||||
return fmt.Sprintf("[%s] %s: %v", ue.Type, ue.Message, ue.Cause)
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", ue.Type, ue.Message)
|
||||
}
|
||||
|
||||
// Unwrap 支持错误链
|
||||
func (ue *UpdaterError) Unwrap() error {
|
||||
return ue.Cause
|
||||
}
|
||||
|
||||
// NewUpdaterError 创建新的UpdaterError
|
||||
func NewUpdaterError(errorType ErrorType, message string, cause error) *UpdaterError {
|
||||
return &UpdaterError{
|
||||
Type: errorType,
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
Timestamp: time.Now(),
|
||||
Context: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext 添加上下文信息
|
||||
func (ue *UpdaterError) WithContext(key string, value interface{}) *UpdaterError {
|
||||
ue.Context[key] = value
|
||||
return ue
|
||||
}
|
||||
|
||||
// GetUserFriendlyMessage 获取用户友好的错误消息
|
||||
func (ue *UpdaterError) GetUserFriendlyMessage() string {
|
||||
switch ue.Type {
|
||||
case NetworkError:
|
||||
return "网络连接失败,请检查网络连接后重试"
|
||||
case APIError:
|
||||
return "服务器响应异常,请稍后重试或联系技术支持"
|
||||
case FileError:
|
||||
return "文件操作失败,请检查文件权限和磁盘空间"
|
||||
case ConfigError:
|
||||
return "配置文件错误,程序将使用默认配置"
|
||||
case InstallError:
|
||||
return "安装过程中出现错误,程序将尝试回滚更改"
|
||||
default:
|
||||
return "发生未知错误,请联系技术支持"
|
||||
}
|
||||
}
|
||||
|
||||
// RetryConfig 重试配置
|
||||
type RetryConfig struct {
|
||||
MaxRetries int
|
||||
InitialDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
BackoffFactor float64
|
||||
RetryableErrors []ErrorType
|
||||
}
|
||||
|
||||
// DefaultRetryConfig 默认重试配置
|
||||
func DefaultRetryConfig() *RetryConfig {
|
||||
return &RetryConfig{
|
||||
MaxRetries: 3,
|
||||
InitialDelay: time.Second,
|
||||
MaxDelay: 30 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
RetryableErrors: []ErrorType{NetworkError, APIError},
|
||||
}
|
||||
}
|
||||
|
||||
// IsRetryable 检查错误是否可重试
|
||||
func (rc *RetryConfig) IsRetryable(err error) bool {
|
||||
if ue, ok := err.(*UpdaterError); ok {
|
||||
for _, retryableType := range rc.RetryableErrors {
|
||||
if ue.Type == retryableType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CalculateDelay 计算重试延迟时间
|
||||
func (rc *RetryConfig) CalculateDelay(attempt int) time.Duration {
|
||||
delay := time.Duration(float64(rc.InitialDelay) * pow(rc.BackoffFactor, float64(attempt)))
|
||||
if delay > rc.MaxDelay {
|
||||
delay = rc.MaxDelay
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
// pow 简单的幂运算实现
|
||||
func pow(base, exp float64) float64 {
|
||||
result := 1.0
|
||||
for i := 0; i < int(exp); i++ {
|
||||
result *= base
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RetryableOperation 可重试的操作函数类型
|
||||
type RetryableOperation func() error
|
||||
|
||||
// ExecuteWithRetry 执行带重试的操作
|
||||
func ExecuteWithRetry(operation RetryableOperation, config *RetryConfig) error {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt <= config.MaxRetries; attempt++ {
|
||||
err := operation()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// 如果不是可重试的错误,直接返回
|
||||
if !config.IsRetryable(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果已经是最后一次尝试,不再等待
|
||||
if attempt == config.MaxRetries {
|
||||
break
|
||||
}
|
||||
|
||||
// 计算延迟时间并等待
|
||||
delay := config.CalculateDelay(attempt)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// ErrorHandler 错误处理器接口
|
||||
type ErrorHandler interface {
|
||||
HandleError(err error) error
|
||||
ShouldRetry(err error) bool
|
||||
GetUserMessage(err error) string
|
||||
}
|
||||
|
||||
// DefaultErrorHandler 默认错误处理器
|
||||
type DefaultErrorHandler struct {
|
||||
retryConfig *RetryConfig
|
||||
}
|
||||
|
||||
// NewDefaultErrorHandler 创建默认错误处理器
|
||||
func NewDefaultErrorHandler() *DefaultErrorHandler {
|
||||
return &DefaultErrorHandler{
|
||||
retryConfig: DefaultRetryConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleError 处理错误
|
||||
func (h *DefaultErrorHandler) HandleError(err error) error {
|
||||
if ue, ok := err.(*UpdaterError); ok {
|
||||
// 记录错误上下文
|
||||
ue.WithContext("handled_at", time.Now())
|
||||
return ue
|
||||
}
|
||||
|
||||
// 将普通错误包装为UpdaterError
|
||||
return NewUpdaterError(NetworkError, "未分类错误", err)
|
||||
}
|
||||
|
||||
// ShouldRetry 判断是否应该重试
|
||||
func (h *DefaultErrorHandler) ShouldRetry(err error) bool {
|
||||
return h.retryConfig.IsRetryable(err)
|
||||
}
|
||||
|
||||
// GetUserMessage 获取用户友好的错误消息
|
||||
func (h *DefaultErrorHandler) GetUserMessage(err error) string {
|
||||
if ue, ok := err.(*UpdaterError); ok {
|
||||
return ue.GetUserFriendlyMessage()
|
||||
}
|
||||
return "发生未知错误,请联系技术支持"
|
||||
}
|
||||
287
Go_Updater/errors/errors_test.go
Normal file
287
Go_Updater/errors/errors_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUpdaterError_Error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *UpdaterError
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "error with cause",
|
||||
err: &UpdaterError{
|
||||
Type: NetworkError,
|
||||
Message: "connection failed",
|
||||
Cause: fmt.Errorf("timeout"),
|
||||
},
|
||||
expected: "[NetworkError] connection failed: timeout",
|
||||
},
|
||||
{
|
||||
name: "error without cause",
|
||||
err: &UpdaterError{
|
||||
Type: APIError,
|
||||
Message: "invalid response",
|
||||
Cause: nil,
|
||||
},
|
||||
expected: "[APIError] invalid response",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.Error(); got != tt.expected {
|
||||
t.Errorf("UpdaterError.Error() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUpdaterError(t *testing.T) {
|
||||
cause := fmt.Errorf("original error")
|
||||
err := NewUpdaterError(FileError, "test message", cause)
|
||||
|
||||
if err.Type != FileError {
|
||||
t.Errorf("Expected type %v, got %v", FileError, err.Type)
|
||||
}
|
||||
if err.Message != "test message" {
|
||||
t.Errorf("Expected message 'test message', got '%v'", err.Message)
|
||||
}
|
||||
if err.Cause != cause {
|
||||
t.Errorf("Expected cause %v, got %v", cause, err.Cause)
|
||||
}
|
||||
if err.Context == nil {
|
||||
t.Error("Expected context to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdaterError_WithContext(t *testing.T) {
|
||||
err := NewUpdaterError(ConfigError, "test", nil)
|
||||
err.WithContext("key1", "value1").WithContext("key2", 42)
|
||||
|
||||
if err.Context["key1"] != "value1" {
|
||||
t.Errorf("Expected context key1 to be 'value1', got %v", err.Context["key1"])
|
||||
}
|
||||
if err.Context["key2"] != 42 {
|
||||
t.Errorf("Expected context key2 to be 42, got %v", err.Context["key2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdaterError_GetUserFriendlyMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
errorType ErrorType
|
||||
expected string
|
||||
}{
|
||||
{NetworkError, "网络连接失败,请检查网络连接后重试"},
|
||||
{APIError, "服务器响应异常,请稍后重试或联系技术支持"},
|
||||
{FileError, "文件操作失败,请检查文件权限和磁盘空间"},
|
||||
{ConfigError, "配置文件错误,程序将使用默认配置"},
|
||||
{InstallError, "安装过程中出现错误,程序将尝试回滚更改"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.errorType.String(), func(t *testing.T) {
|
||||
err := NewUpdaterError(tt.errorType, "test", nil)
|
||||
if got := err.GetUserFriendlyMessage(); got != tt.expected {
|
||||
t.Errorf("GetUserFriendlyMessage() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryConfig_IsRetryable(t *testing.T) {
|
||||
config := DefaultRetryConfig()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "retryable network error",
|
||||
err: NewUpdaterError(NetworkError, "test", nil),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "retryable api error",
|
||||
err: NewUpdaterError(APIError, "test", nil),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-retryable file error",
|
||||
err: NewUpdaterError(FileError, "test", nil),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "regular error",
|
||||
err: fmt.Errorf("regular error"),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := config.IsRetryable(tt.err); got != tt.expected {
|
||||
t.Errorf("IsRetryable() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryConfig_CalculateDelay(t *testing.T) {
|
||||
config := DefaultRetryConfig()
|
||||
|
||||
tests := []struct {
|
||||
attempt int
|
||||
expected time.Duration
|
||||
}{
|
||||
{0, time.Second},
|
||||
{1, 2 * time.Second},
|
||||
{2, 4 * time.Second},
|
||||
{10, 30 * time.Second}, // should be capped at MaxDelay
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("attempt_%d", tt.attempt), func(t *testing.T) {
|
||||
if got := config.CalculateDelay(tt.attempt); got != tt.expected {
|
||||
t.Errorf("CalculateDelay(%d) = %v, want %v", tt.attempt, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteWithRetry(t *testing.T) {
|
||||
config := DefaultRetryConfig()
|
||||
config.InitialDelay = time.Millisecond // 加快测试速度
|
||||
|
||||
t.Run("success on first try", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
return nil
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("Expected 1 attempt, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success after retries", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return NewUpdaterError(NetworkError, "temporary failure", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Errorf("Expected 3 attempts, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-retryable error", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
return NewUpdaterError(FileError, "file not found", nil)
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("Expected 1 attempt, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max retries exceeded", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
return NewUpdaterError(NetworkError, "persistent failure", nil)
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
expectedAttempts := config.MaxRetries + 1
|
||||
if attempts != expectedAttempts {
|
||||
t.Errorf("Expected %d attempts, got %d", expectedAttempts, attempts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultErrorHandler(t *testing.T) {
|
||||
handler := NewDefaultErrorHandler()
|
||||
|
||||
t.Run("handle updater error", func(t *testing.T) {
|
||||
originalErr := NewUpdaterError(NetworkError, "test", nil)
|
||||
handledErr := handler.HandleError(originalErr)
|
||||
|
||||
if handledErr != originalErr {
|
||||
t.Error("Expected same error instance")
|
||||
}
|
||||
if originalErr.Context["handled_at"] == nil {
|
||||
t.Error("Expected handled_at context to be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handle regular error", func(t *testing.T) {
|
||||
originalErr := fmt.Errorf("regular error")
|
||||
handledErr := handler.HandleError(originalErr)
|
||||
|
||||
if ue, ok := handledErr.(*UpdaterError); ok {
|
||||
if ue.Type != NetworkError {
|
||||
t.Errorf("Expected NetworkError, got %v", ue.Type)
|
||||
}
|
||||
if ue.Cause != originalErr {
|
||||
t.Error("Expected original error as cause")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected UpdaterError")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should retry", func(t *testing.T) {
|
||||
retryableErr := NewUpdaterError(NetworkError, "test", nil)
|
||||
nonRetryableErr := NewUpdaterError(FileError, "test", nil)
|
||||
|
||||
if !handler.ShouldRetry(retryableErr) {
|
||||
t.Error("Expected network error to be retryable")
|
||||
}
|
||||
if handler.ShouldRetry(nonRetryableErr) {
|
||||
t.Error("Expected file error to not be retryable")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get user message", func(t *testing.T) {
|
||||
updaterErr := NewUpdaterError(NetworkError, "test", nil)
|
||||
regularErr := fmt.Errorf("regular error")
|
||||
|
||||
userMsg1 := handler.GetUserMessage(updaterErr)
|
||||
userMsg2 := handler.GetUserMessage(regularErr)
|
||||
|
||||
if userMsg1 != "网络连接失败,请检查网络连接后重试" {
|
||||
t.Errorf("Unexpected user message: %s", userMsg1)
|
||||
}
|
||||
if userMsg2 != "发生未知错误,请联系技术支持" {
|
||||
t.Errorf("Unexpected user message: %s", userMsg2)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user