I’m trying to write a simple program that can do self-update and restart automatically, I wrote the below code and was able to check if there is any updated version at Github, download it, and replace the current executable with the downloaded one:
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
}
But I have to exit the program manually then start again to get the updated version.
Is there a way that I can close the program then restart it, or is there another approach to get this done smoothly, not sure if I have to have two programs, one to handle the update and one to run, or if everything can be handled in a single app.