Golang应用如何实现自我更新后自动重启

Golang应用如何实现自我更新后自动重启 我正在尝试编写一个能够自我更新并自动重启的简单程序。我编写了以下代码,能够检查GitHub上是否有更新版本,下载它,并用下载的文件替换当前的可执行文件:

package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path"
	"strings"
	"time"
)

var version = "v1.0.0"

func downloadFile(filepath string, url string) error {
	// Check if file already exists
	if _, err := os.Stat(filepath); err == nil {
		return fmt.Errorf("file %s already exists", filepath)
	}

	// Check if directory exists, if not create it
	dir := path.Dir(filepath)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		os.MkdirAll(dir, os.ModePerm)
	}

	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	// Create a custom http client
	var netTransport = &http.Transport{
		Dial: (&net.Dialer{
			Timeout: 5 * time.Second,
		}).Dial,
		TLSHandshakeTimeout: 5 * time.Second,
	}
	var netClient = &http.Client{
		Timeout:   time.Second * 10,
		Transport: netTransport,
	}

	// Get the data
	resp, err := netClient.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Write the body to file
	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return err
	}

	return nil
}

type Release struct {
	TagName string `json:"tag_name"`
	Assets  []struct {
		BrowserDownloadUrl string `json:"browser_download_url"`
	} `json:"assets"`
}

func getLatestRelease(repoUrl string) (*Release, error) {
	resp, err := http.Get(repoUrl)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var release Release
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return nil, err
	}

	return &release, nil
}

func checkForUpdates() {
	repoUrl := "https://api.github.com/repos/HasanAbuKaram/testGoupdate/releases/latest"
	release, err := getLatestRelease(repoUrl)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	if release.TagName != version {
		fmt.Println("A new version is available:", release.TagName)
		fmt.Println("Options:")
		fmt.Println("a. Remind me now")
		fmt.Println("b. Download new file but do not install")
		fmt.Println("c. Download and install")

		var option string
		fmt.Print("Enter your choice (a/b/c): ")
		fmt.Scanln(&option)

		// Convert the user's input to lowercase
		option = strings.ToLower(option)

		switch option {
		case "a":
			fmt.Println("Remind me later")
		case "b":
			var url = release.Assets[0].BrowserDownloadUrl
			err := downloadFile(fmt.Sprintf("./bin/%v.exe", release.TagName), url)
			if err != nil {
				fmt.Println("Error downloading file:", err)
			}
			fmt.Println("File downloaded but not installed.")
		case "c":
			var url = release.Assets[0].BrowserDownloadUrl
			err := downloadFile(fmt.Sprintf("./bin/%v.exe", release.TagName), url)
			if err != nil {
				fmt.Println("Error downloading file:", err)
			} else {
				fmt.Println("File downloaded and installed successfully.")
				// Install the downloaded binary
				if err := installBinary(fmt.Sprintf("./bin/%v.exe", release.TagName)); err != nil {
					fmt.Println("Error installing binary:", err)
				}
			}
		default:
			fmt.Println("Invalid option")
		}
	} else {
		fmt.Println("You are running the latest version: ", version)
	}
}

func main() {
	fmt.Printf("Hello, World! Running version %s\n", version)

	// Perform the first update check immediately
	checkForUpdates()

	// Then check for updates every hour
	ticker := time.NewTicker(1 * time.Hour)
	defer ticker.Stop()

	// Create a channel to listen for incoming signals
	sigs := make(chan os.Signal, 1)

	// Register the channel to receive os.Interrupt signals
	signal.Notify(sigs, os.Interrupt)

	go func() {
		for {
			// Wait for an os.Interrupt signal
			sig := <-sigs

			// Ask for user input when an os.Interrupt signal is received
			if sig == os.Interrupt {
				reader := bufio.NewReader(os.Stdin)
				fmt.Print("Are you sure you want to exit? (y/n): ")
				text, _ := reader.ReadString('\n')
				text = strings.TrimSpace(text) // remove leading and trailing whitespace
				if text == "y" || text == "Y" {
					fmt.Println("Exiting...")
					os.Exit(0)
				} else {
					fmt.Println("Continuing...")
				}
			}
		}
	}()

	for {
		<-ticker.C
		checkForUpdates()
	}
}

func installBinary(filepath string) error {
	// Rename the current executable
	currentExec, err := os.Executable()
	if err != nil {
		return err
	}

	backupPath := currentExec + ".bak"
	if err := os.Rename(currentExec, backupPath); err != nil {
		return err
	}

	// Replace the current executable with the new one
	if err := os.Rename(filepath, currentExec); err != nil {
		// Restore the original executable if there's an error
		os.Rename(backupPath, currentExec)
		return err
	}

	// Delete the backup file
	os.Remove(backupPath)

	return nil
}

但我必须手动退出程序然后重新启动才能获得更新后的版本。

有没有办法让我关闭程序然后重新启动它,或者是否有其他方法可以平滑地完成这个操作?我不确定是否需要两个程序,一个处理更新,一个运行,或者是否所有操作都可以在一个应用程序中处理。


更多关于Golang应用如何实现自我更新后自动重启的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

感谢分享这段用于下载版本的代码,直接运行即可使用…

更多关于Golang应用如何实现自我更新后自动重启的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


syscall.Exec 调用的是 execve(2),这很可能就是你在这里想要的功能。

啊,抱歉,我忽略了您使用的是 Windows 系统。是的,这是一个 POSIX 系统调用,而 Windows 在 WinNT 之后的某个时间点就放弃了 POSIX。

我不确定在 Win32 中是否有任何东西可以替代它(但我自 NT 之后就再没接触过 Win32)。微软对此类问题的“解决方案”似乎是 WSL,但这显然对试图向普通用户分发软件的人没有帮助。

嗯,看起来在 Windows 上它不起作用:

我写了:

	// Get the path to the currently running executable
	executablePath, err := os.Executable()
	if err != nil {
		fmt.Println("Error:", err)
		return err
	}

	// Call syscall.Exec to restart the application
	err = syscall.Exec(executablePath, os.Args, os.Environ())
	if err != nil {
		fmt.Println("Error:", err)
	}

但得到了错误:

Error: not supported by windows

我本以为这段代码可能会失败:

hyousef:

if err := os.Rename(currentExec, backupPath); err != nil {
	return err
}

另外,这段代码能直接运行然后退出原进程,这很有趣:

hyousef:

cmd := exec.Command(executablePath, os.Args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin

err = cmd.Start()

非常酷。 感谢分享!

摆弄底层的系统调用并不太具备平台独立性。

Go 标准库也正在逐步淘汰来自 https://pkg.go.dev/syscall 的 syscall,转而使用 https://pkg.go.dev/golang.org/x/sys

也许你也能在 Windows 上找到一些东西来替换当前进程,但所有这些听起来都让我觉得非常棘手。

我认为采用另一种方案,使用两个独立的应用程序(一个更新器/启动器和实际的应用本身)会更健壮,因为这样你能更好地处理错误。根据你的需求,你可以把它做得很复杂(例如,让应用程序的新旧版本相互通信,以确保在终止旧版本之前一切正常),但一个简单的启动器,在启动时检查是否有新版本,可能就足够简单和用户友好了,除非它可能是一个长时间运行的进程?

感谢您的支持,以下方法对我有效,但不确定在 Windows 上是否是最佳方式,不过目前运行正常:

package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"path"
	"strings"
	"time"
)

var version = "v1.0.0"

func downloadFile(filepath string, url string) error {
	// Check if file already exists
	if _, err := os.Stat(filepath); err == nil {
		return fmt.Errorf("file %s already exists", filepath)
	}

	// Check if directory exists, if not create it
	dir := path.Dir(filepath)
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		os.MkdirAll(dir, os.ModePerm)
	}

	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	// Create a custom http client
	var netTransport = &http.Transport{
		Dial: (&net.Dialer{
			Timeout: 5 * time.Second,
		}).Dial,
		TLSHandshakeTimeout: 5 * time.Second,
	}
	var netClient = &http.Client{
		Timeout:   time.Second * 10,
		Transport: netTransport,
	}

	// Get the data
	resp, err := netClient.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Write the body to file
	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return err
	}

	return nil
}

type Release struct {
	TagName string `json:"tag_name"`
	Assets  []struct {
		BrowserDownloadUrl string `json:"browser_download_url"`
	} `json:"assets"`
}

func getLatestRelease(repoUrl string) (*Release, error) {
	resp, err := http.Get(repoUrl)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var release Release
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return nil, err
	}

	return &release, nil
}

func checkForUpdates() {
	repoUrl := "https://api.github.com/repos/HasanAbuKaram/testGoupdate/releases/latest"
	release, err := getLatestRelease(repoUrl)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	if release.TagName != version {
		fmt.Println("A new version is available:", release.TagName)
		fmt.Println("Options:")
		fmt.Println("a. Remind me now")
		fmt.Println("b. Download new file but do not install")
		fmt.Println("c. Download and install")

		var option string
		fmt.Print("Enter your choice (a/b/c): ")
		fmt.Scanln(&option)

		// Convert the user's input to lowercase
		option = strings.ToLower(option)

		switch option {
		case "a":
			fmt.Println("Remind me later")
		case "b":
			var url = release.Assets[0].BrowserDownloadUrl
			err := downloadFile(fmt.Sprintf("./bin/%v.exe", release.TagName), url)
			if err != nil {
				fmt.Println("Error downloading file:", err)
			}
			fmt.Println("File downloaded but not installed.")
		case "c":
			var url = release.Assets[0].BrowserDownloadUrl
			err := downloadFile(fmt.Sprintf("./bin/%v.exe", release.TagName), url)
			if err != nil {
				fmt.Println("Error downloading file:", err)
			} else {
				fmt.Println("File downloaded and installed successfully.")
				// Install the downloaded binary
				if err := installBinary(fmt.Sprintf("./bin/%v.exe", release.TagName)); err != nil {
					fmt.Println("Error installing binary:", err)
				}
			}
		default:
			fmt.Println("Invalid option")
		}
	} else {
		fmt.Println("You are running the latest version: ", version)
	}
}

func main() {
	fmt.Printf("Hello, World! Running version %s\n", version)

	// Perform the first update check immediately
	checkForUpdates()

	// Then check for updates every hour
	ticker := time.NewTicker(1 * time.Hour)
	defer ticker.Stop()

	// Create a channel to listen for incoming signals
	sigs := make(chan os.Signal, 1)

	// Register the channel to receive os.Interrupt signals
	signal.Notify(sigs, os.Interrupt)

	go func() {
		for {
			// Wait for an os.Interrupt signal
			sig := <-sigs

			// Ask for user input when an os.Interrupt signal is received
			if sig == os.Interrupt {
				reader := bufio.NewReader(os.Stdin)
				fmt.Print("Are you sure you want to exit? (y/n): ")
				text, _ := reader.ReadString('\n')
				text = strings.TrimSpace(text) // remove leading and trailing whitespace
				if text == "y" || text == "Y" {
					fmt.Println("Exiting...")
					os.Exit(0)
				} else {
					fmt.Println("Continuing...")
				}
			}
		}
	}()

	for {
		<-ticker.C
		checkForUpdates()
	}
}

func installBinary(filepath string) error {
	// Rename the current executable
	currentExec, err := os.Executable()
	if err != nil {
		return err
	}

	backupPath := currentExec + ".bak"
	if err := os.Rename(currentExec, backupPath); err != nil {
		return err
	}

	// Replace the current executable with the new one
	if err := os.Rename(filepath, currentExec); err != nil {
		// Restore the original executable if there's an error
		os.Rename(backupPath, currentExec)
		return err
	}

	// Delete the backup file
	os.Remove(backupPath)

	// Get the path to the currently running executable
	executablePath, err := os.Executable()
	if err != nil {
		fmt.Println("Error:", err)
		return err
	}

	// Start a new instance of the application
	cmd := exec.Command(executablePath, os.Args[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin

	err = cmd.Start()
	if err != nil {
		fmt.Println("Error starting process:", err)
		return err
	}

	// Exit the current instance
	os.Exit(0)

	return nil
}

要实现Go应用程序的自我更新并自动重启,可以使用子进程管理的方式。以下是修改后的代码示例,通过启动新进程并优雅退出当前进程来实现平滑重启:

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "path"
    "path/filepath"
    "runtime"
    "strings"
    "syscall"
    "time"
)

var version = "v1.0.0"

func downloadFile(filepath string, url string) error {
    if _, err := os.Stat(filepath); err == nil {
        return fmt.Errorf("file %s already exists", filepath)
    }

    dir := path.Dir(filepath)
    if _, err := os.Stat(dir); os.IsNotExist(err) {
        os.MkdirAll(dir, os.ModePerm)
    }

    out, err := os.Create(filepath)
    if err != nil {
        return err
    }
    defer out.Close()

    var netTransport = &http.Transport{
        Dial: (&net.Dialer{
            Timeout: 5 * time.Second,
        }).Dial,
        TLSHandshakeTimeout: 5 * time.Second,
    }
    var netClient = &http.Client{
        Timeout:   time.Second * 10,
        Transport: netTransport,
    }

    resp, err := netClient.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    _, err = io.Copy(out, resp.Body)
    return err
}

type Release struct {
    TagName string `json:"tag_name"`
    Assets  []struct {
        BrowserDownloadUrl string `json:"browser_download_url"`
    } `json:"assets"`
}

func getLatestRelease(repoUrl string) (*Release, error) {
    resp, err := http.Get(repoUrl)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var release Release
    if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
        return nil, err
    }

    return &release, nil
}

func restartApplication() error {
    self, err := os.Executable()
    if err != nil {
        return err
    }

    args := os.Args
    env := os.Environ()

    cmd := exec.Command(self, args[1:]...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Stdin = os.Stdin
    cmd.Env = env
    cmd.Dir, _ = os.Getwd()

    if err := cmd.Start(); err != nil {
        return err
    }

    os.Exit(0)
    return nil
}

func checkForUpdates() bool {
    repoUrl := "https://api.github.com/repos/HasanAbuKaram/testGoupdate/releases/latest"
    release, err := getLatestRelease(repoUrl)
    if err != nil {
        fmt.Println("Error:", err)
        return false
    }

    if release.TagName != version {
        fmt.Println("A new version is available:", release.TagName)
        fmt.Println("Options:")
        fmt.Println("a. Remind me now")
        fmt.Println("b. Download new file but do not install")
        fmt.Println("c. Download, install and restart")

        var option string
        fmt.Print("Enter your choice (a/b/c): ")
        fmt.Scanln(&option)

        option = strings.ToLower(option)

        switch option {
        case "a":
            fmt.Println("Remind me later")
        case "b":
            var url = release.Assets[0].BrowserDownloadUrl
            err := downloadFile(fmt.Sprintf("./bin/%v.exe", release.TagName), url)
            if err != nil {
                fmt.Println("Error downloading file:", err)
            }
            fmt.Println("File downloaded but not installed.")
        case "c":
            var url = release.Assets[0].BrowserDownloadUrl
            err := downloadFile(fmt.Sprintf("./bin/%v.exe", release.TagName), url)
            if err != nil {
                fmt.Println("Error downloading file:", err)
            } else {
                fmt.Println("File downloaded successfully.")
                if err := installBinary(fmt.Sprintf("./bin/%v.exe", release.TagName)); err != nil {
                    fmt.Println("Error installing binary:", err)
                } else {
                    fmt.Println("Restarting application...")
                    time.Sleep(2 * time.Second)
                    return true
                }
            }
        default:
            fmt.Println("Invalid option")
        }
    } else {
        fmt.Println("You are running the latest version: ", version)
    }
    return false
}

func installBinary(filepath string) error {
    currentExec, err := os.Executable()
    if err != nil {
        return err
    }

    if runtime.GOOS == "windows" {
        return installBinaryWindows(currentExec, filepath)
    }
    return installBinaryUnix(currentExec, filepath)
}

func installBinaryWindows(currentExec, newBinary string) error {
    backupPath := currentExec + ".bak"
    
    if err := os.Rename(currentExec, backupPath); err != nil {
        return err
    }

    if err := os.Rename(newBinary, currentExec); err != nil {
        os.Rename(backupPath, currentExec)
        return err
    }

    os.Remove(backupPath)
    return nil
}

func installBinaryUnix(currentExec, newBinary string) error {
    tempPath := currentExec + ".tmp"
    
    if err := os.Rename(currentExec, tempPath); err != nil {
        return err
    }

    if err := os.Rename(newBinary, currentExec); err != nil {
        os.Rename(tempPath, currentExec)
        return err
    }

    if err := os.Chmod(currentExec, 0755); err != nil {
        return err
    }

    os.Remove(tempPath)
    return nil
}

func main() {
    fmt.Printf("Hello, World! Running version %s\n", version)

    if checkForUpdates() {
        if err := restartApplication(); err != nil {
            fmt.Println("Error restarting application:", err)
        }
        return
    }

    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()

    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)

    go func() {
        for {
            sig := <-sigs
            if sig == os.Interrupt || sig == syscall.SIGTERM {
                reader := bufio.NewReader(os.Stdin)
                fmt.Print("Are you sure you want to exit? (y/n): ")
                text, _ := reader.ReadString('\n')
                text = strings.TrimSpace(text)
                if text == "y" || text == "Y" {
                    fmt.Println("Exiting...")
                    os.Exit(0)
                } else {
                    fmt.Println("Continuing...")
                }
            }
        }
    }()

    for {
        <-ticker.C
        if checkForUpdates() {
            if err := restartApplication(); err != nil {
                fmt.Println("Error restarting application:", err)
            }
            break
        }
    }
}

对于更复杂的场景,可以考虑使用双进程架构。以下是分离更新器和主程序的示例:

// updater.go
package main

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

func main() {
    exePath, _ := os.Executable()
    exeDir := filepath.Dir(exePath)
    mainApp := filepath.Join(exeDir, "mainapp")
    
    for {
        cmd := exec.Command(mainApp)
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.Stdin = os.Stdin
        
        if err := cmd.Start(); err != nil {
            fmt.Printf("Failed to start main app: %v\n", err)
            time.Sleep(5 * time.Second)
            continue
        }
        
        if err := cmd.Wait(); err != nil {
            fmt.Printf("Main app exited: %v\n", err)
            
            if cmd.ProcessState.ExitCode() == 42 {
                fmt.Println("Restarting after update...")
                continue
            }
        }
        
        time.Sleep(5 * time.Second)
    }
}

// mainapp.go
package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    fmt.Println("Main application running...")
    
    if shouldUpdate() {
        performUpdate()
        os.Exit(42)
    }
    
    for {
        time.Sleep(time.Second)
    }
}

func shouldUpdate() bool {
    return false
}

func performUpdate() {
    fmt.Println("Performing update...")
}

对于跨平台兼容性,可以使用以下重启函数:

func restartSelf() error {
    executable, err := os.Executable()
    if err != nil {
        return err
    }
    
    args := os.Args
    env := os.Environ()
    
    cmd := &exec.Cmd{
        Path:   executable,
        Args:   args,
        Env:    env,
        Stdin:  os.Stdin,
        Stdout: os.Stdout,
        Stderr: os.Stderr,
    }
    
    if err := cmd.Start(); err != nil {
        return err
    }
    
    os.Exit(0)
    return nil
}
回到顶部