144 lines
4.1 KiB
Go
144 lines
4.1 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"scout/pkg/scoutpkg"
|
|
)
|
|
|
|
var ErrPlatformInstallRequired = errors.New("当前平台需要宿主应用接入系统安装流程")
|
|
|
|
// LaunchRequest 描述平台适配器启动已确认版本所需的信息。
|
|
type LaunchRequest struct {
|
|
Release ReleasePointer
|
|
Args []string
|
|
Env []string
|
|
}
|
|
|
|
// PlatformAdapter 隔离更新器与不同操作系统的启动和安装机制。
|
|
// Prepare、Activate 和健康确认仍由 Manager 统一负责,宿主应用只需在需要时注入适配器。
|
|
type PlatformAdapter interface {
|
|
Platform() string
|
|
Validate(ReleasePointer) error
|
|
Install(context.Context, ReleasePointer) error
|
|
Launch(context.Context, LaunchRequest) (*exec.Cmd, error)
|
|
}
|
|
|
|
type nativeAdapter struct {
|
|
platform string
|
|
}
|
|
|
|
type androidAdapter struct{}
|
|
|
|
// NewPlatformAdapter 返回指定平台的默认适配器。
|
|
func NewPlatformAdapter(platform string) (PlatformAdapter, error) {
|
|
normalized, err := scoutpkg.NormalizePlatform(platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if normalized == "android" {
|
|
return androidAdapter{}, nil
|
|
}
|
|
return nativeAdapter{platform: normalized}, nil
|
|
}
|
|
|
|
// NewNativeAdapter 创建桌面平台默认适配器,便于宿主应用显式注入。
|
|
func NewNativeAdapter(platform string) (PlatformAdapter, error) {
|
|
normalized, err := scoutpkg.NormalizePlatform(platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if normalized == "android" {
|
|
return nil, errors.New("Android 必须使用宿主应用提供的安装适配器")
|
|
}
|
|
return nativeAdapter{platform: normalized}, nil
|
|
}
|
|
|
|
func (adapter nativeAdapter) Platform() string {
|
|
return adapter.platform
|
|
}
|
|
|
|
func (adapter nativeAdapter) Validate(release ReleasePointer) error {
|
|
if release.Platform != "" && release.Platform != adapter.platform && release.Platform != "any" {
|
|
return fmt.Errorf("更新版本平台不匹配: %s", release.Platform)
|
|
}
|
|
executable, err := releaseExecutable(release)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := os.Stat(executable)
|
|
if err != nil {
|
|
return fmt.Errorf("平台适配器检查程序入口失败: %w", err)
|
|
}
|
|
if info.IsDir() {
|
|
return errors.New("程序入口不能是目录")
|
|
}
|
|
if adapter.platform == "macos" && strings.HasSuffix(strings.ToLower(executable), ".app") {
|
|
return errors.New("macOS 程序入口必须指向应用包内的可执行文件")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (adapter nativeAdapter) Install(_ context.Context, release ReleasePointer) error {
|
|
return adapter.Validate(release)
|
|
}
|
|
|
|
func (adapter nativeAdapter) Launch(ctx context.Context, request LaunchRequest) (*exec.Cmd, error) {
|
|
if err := adapter.Validate(request.Release); err != nil {
|
|
return nil, err
|
|
}
|
|
executable, err := releaseExecutable(request.Release)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
command := exec.CommandContext(ctx, executable, request.Args...)
|
|
command.Dir = request.Release.Path
|
|
command.Env = request.Env
|
|
if err := command.Start(); err != nil {
|
|
return nil, fmt.Errorf("启动当前版本失败: %w", err)
|
|
}
|
|
return command, nil
|
|
}
|
|
|
|
func (androidAdapter) Platform() string {
|
|
return "android"
|
|
}
|
|
|
|
func (androidAdapter) Validate(release ReleasePointer) error {
|
|
if strings.TrimSpace(release.Path) == "" {
|
|
return errors.New("Android 更新版本路径不能为空")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (adapter androidAdapter) Install(_ context.Context, release ReleasePointer) error {
|
|
if err := adapter.Validate(release); err != nil {
|
|
return err
|
|
}
|
|
return ErrPlatformInstallRequired
|
|
}
|
|
|
|
func (androidAdapter) Launch(_ context.Context, _ LaunchRequest) (*exec.Cmd, error) {
|
|
return nil, errors.New("Android 不能由 Go 更新器直接启动,请由宿主应用通过系统组件启动")
|
|
}
|
|
|
|
func releaseExecutable(release ReleasePointer) (string, error) {
|
|
if strings.TrimSpace(release.Path) == "" {
|
|
return "", errors.New("版本路径不能为空")
|
|
}
|
|
if strings.TrimSpace(release.Entrypoint) == "" {
|
|
return "", errors.New("程序入口不能为空")
|
|
}
|
|
root, err := filepath.Abs(release.Path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("解析版本路径失败: %w", err)
|
|
}
|
|
return safeEntrypoint(root, release.Entrypoint)
|
|
}
|