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
感谢分享这段用于下载版本的代码,直接运行即可使用…
更多关于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
}

