Golang二进制文件的OTA更新实现

Golang二进制文件的OTA更新实现 有没有办法向使用Go构建的二进制应用程序推送OTA(空中下载)更新?具体如何操作?

我考虑过以下逻辑,但不确定是否可行,特别是第5、6、7点,因为应用程序届时已经关闭了?!

// 1. Check current binary version
// 2. Read latest version from repository
// 3. Download the new version (if any)
// 4. Shut done current instant of the app
// 5. Delete the current app binary from the device
// 6. Move the newly downloaded version to the original path of the app
// 7. Launch an instant of the new version

更多关于Golang二进制文件的OTA更新实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

非常感谢,我会尝试一下。

更多关于Golang二进制文件的OTA更新实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你需要手动创建一个OTA功能吗?

是的

MinIO分布式文件系统项目具备二进制升级功能,并且是使用Go语言编写的。

luk4z7:

我只了解嵌入式项目中的OTA,比如乐鑫。

怎么实现的?我看了你分享的链接,但没找到。

那么,你是否开发任何嵌入式软件?因为有很多工具可以用于部署到服务器。

logo

A curated list of awesome Go frameworks, libraries and software - Awesome Go

一个精心整理的优秀 #Golang 框架、库和软件列表

或者你需要手动创建一个 OTA 功能吗?

ESP-IDF OTA 文档 FreeRTOS OTA 开发人员指南

但这些链接与 Go 语言没有任何关系。

我明白了。据我所知,目前没有用Go语言实现这个功能的项目。我只了解嵌入式项目中的OTA(空中下载技术),比如Espressif、FreeRTOS和IOT Core等等,这涉及到客户端和服务器端。现在如果你需要创建这个功能,我认为服务器端更常见,但客户端端则取决于设备类型,例如ESP32,以及你通过何种方式(如蓝牙、MQTT等)来发送OTA字节数据。

我之前实现过这个功能,但当时仅针对单一操作系统(CentOS)和一个持续运行的服务。我是通过以下步骤完成的:

  • 在应用程序中启动一个goroutine,按照预定义的时间间隔检查新版本(具体间隔根据你的需求而定)。
  • 如果存在新版本,则获取新的二进制文件和静态内容(我有构建二进制文件的工具;如果没有,你可以获取源代码并构建新的二进制文件。显然,如果你想构建二进制文件,就需要在目标机器上安装Go作为依赖)。
  • 此时,通过 exec.Command 执行一个更新脚本,该脚本:
    • 运行数据库更新(不确定你是否有数据层,但大多数应用程序都有)。
    • 确保新的应用程序二进制文件具有 +x 可执行权限。
    • 将新二进制文件复制覆盖当前正在运行的二进制文件(CentOS允许这样做)。
    • 运行 systemctl restart $myService 来重启服务并使用新的二进制文件。

总而言之:实现此功能的一个简单方法是随应用程序附带一个更新器。它可以是一个脚本,也可以是它自己的二进制文件。根据你构建的应用程序类型(它是一个持续运行的服务吗?还是一个用户启动的GUI应用程序?等等),你需要让更新器以不同的方式工作。

对于Go二进制文件的OTA更新,你的思路基本正确,但需要处理自我替换的问题。以下是实现方案:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
)

// 主程序更新逻辑
func updateBinary() error {
    // 1. 检查当前版本
    currentVersion := getCurrentVersion()
    
    // 2. 从仓库获取最新版本
    latestVersion, err := fetchLatestVersion()
    if err != nil {
        return err
    }
    
    if currentVersion >= latestVersion {
        return nil // 无需更新
    }
    
    // 3. 下载新版本
    newBinaryPath, err := downloadNewVersion(latestVersion)
    if err != nil {
        return err
    }
    
    // 4. 启动更新器进程进行替换
    return launchUpdater(newBinaryPath)
}

// 下载新版本
func downloadNewVersion(version string) (string, error) {
    url := fmt.Sprintf("https://example.com/app/v%s/app-%s-%s", 
        version, runtime.GOOS, runtime.GOARCH)
    
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    // 创建临时文件
    tmpFile, err := os.CreateTemp("", "update-*.tmp")
    if err != nil {
        return "", err
    }
    defer tmpFile.Close()
    
    // 写入临时文件
    _, err = io.Copy(tmpFile, resp.Body)
    if err != nil {
        return "", err
    }
    
    // 设置可执行权限
    if runtime.GOOS != "windows" {
        os.Chmod(tmpFile.Name(), 0755)
    }
    
    return tmpFile.Name(), nil
}

// 启动更新器进程
func launchUpdater(newBinaryPath string) error {
    // 获取当前可执行文件路径
    exePath, err := os.Executable()
    if err != nil {
        return err
    }
    
    // 创建更新器脚本/程序
    updaterScript := createUpdaterScript(exePath, newBinaryPath)
    
    // 启动更新器(独立进程)
    cmd := exec.Command(updaterScript)
    if err := cmd.Start(); err != nil {
        return err
    }
    
    // 主程序退出
    os.Exit(0)
    return nil
}

// 创建更新器脚本
func createUpdaterScript(currentPath, newPath string) string {
    var script string
    
    if runtime.GOOS == "windows" {
        script = fmt.Sprintf(`@echo off
timeout /t 2 /nobreak >nul
del "%s"
move "%s" "%s"
start "" "%s"`,
            currentPath, newPath, currentPath, currentPath)
        
        scriptPath := filepath.Join(os.TempDir(), "updater.bat")
        os.WriteFile(scriptPath, []byte(script), 0644)
        return scriptPath
    } else {
        script = fmt.Sprintf(`#!/bin/bash
sleep 2
rm -f "%s"
mv "%s" "%s"
chmod +x "%s"
"%s" &`,
            currentPath, newPath, currentPath, currentPath, currentPath)
        
        scriptPath := filepath.Join(os.TempDir(), "updater.sh")
        os.WriteFile(scriptPath, []byte(script), 0755)
        return scriptPath
    }
}

// 版本管理函数
func getCurrentVersion() string {
    // 从嵌入信息或配置文件读取
    return "1.0.0"
}

func fetchLatestVersion() (string, error) {
    // 从API或版本文件获取
    resp, err := http.Get("https://example.com/version.txt")
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    
    version, _ := io.ReadAll(resp.Body)
    return string(version), nil
}

对于关键的第5、6、7点,解决方案是:

  1. 使用独立更新器进程:主程序启动一个独立的更新器进程(脚本或Go程序),然后退出
  2. 更新器等待主程序退出:更新器等待几秒确保主程序完全退出
  3. 执行替换操作:更新器删除旧文件,移动新文件到原位置
  4. 启动新版本:更新器启动新版本程序

更健壮的实现可以使用专门的更新器二进制文件:

// updater.go - 独立的更新器程序
package main

import (
    "fmt"
    "os"
    "path/filepath"
    "time"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: updater <current> <new>")
        os.Exit(1)
    }
    
    currentPath := os.Args[1]
    newPath := os.Args[2]
    
    // 等待主程序退出
    time.Sleep(2 * time.Second)
    
    // 删除当前文件
    os.Remove(currentPath)
    
    // 移动新文件
    err := os.Rename(newPath, currentPath)
    if err != nil {
        // 如果跨卷移动失败,使用复制
        copyFile(newPath, currentPath)
        os.Remove(newPath)
    }
    
    // 设置权限
    if runtime.GOOS != "windows" {
        os.Chmod(currentPath, 0755)
    }
    
    // 启动新程序
    cmd := exec.Command(currentPath)
    cmd.Start()
}

func copyFile(src, dst string) error {
    in, err := os.Open(src)
    if err != nil {
        return err
    }
    defer in.Close()
    
    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()
    
    _, err = io.Copy(out, in)
    return err
}

主程序调用方式:

func launchUpdater(newBinaryPath string) error {
    exePath, _ := os.Executable()
    
    // 编译或使用预编译的更新器
    updaterPath := "/path/to/updater"
    
    cmd := exec.Command(updaterPath, exePath, newBinaryPath)
    if err := cmd.Start(); err != nil {
        return err
    }
    
    os.Exit(0)
    return nil
}

这种方法确保了:

  • 主程序完全退出后才进行文件操作
  • 使用独立进程避免文件锁定问题
  • 支持跨平台操作(Windows/Linux/macOS)
  • 提供回滚机制(可保留旧版本备份)
回到顶部