Auto restart after self-update

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.

syscall.Exec calls execve(2), which is probably what you want here.

mm, it looks it is not working at Windows:

I wrote:

	// 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)
	}

But got the error:

Error: not supported by windows

Messing with low-level syscalls is not very platform-independent.

The Go standard library is also phasing out syscall’s from
https://pkg.go.dev/syscall
to
https://pkg.go.dev/golang.org/x/sys

Maybe you can find something to replace the current process also on Windows but it all sounds very finicky to me.

I think a different route with 2 separate apps (an updater/launcher and the actual app) will be more robust since you will be able to handle errors better. You could make it very complex depending on your needs (e.g. letting the new and old version of the apps communicate with each other to make sure all is well before killing the old version) but a simple Launcher that checks if there is a new version at startup, is probably simple and user-friendly enough, unless maybe it’s a long-running process?

Ahh, sorry, I missed that you were on Windows, yeah this is a POSIX system call, and Windows dropped POSIX at some point post WinNT days :frowning:

I’m not sure there’s anything to replace it in Win32 (but I’ve not touched Win32 since NT), Microsoft’s “solution” to these sorts of issues seems to be WSL, which obviously doesn’t help someone trying to distribute software to normal users.

Thanks for your support, the below worked with me, but not sure if it is the optimal way at Windows or not, but it is working so far:

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
}
2 Likes

I would have maybe expected this to fail:

Also interesting how this just works and then exits the original:

Very cool.
Thanks for sharing!

1 Like

Thank you for this sharing code to downloaded version, simply use in run the code…

1 Like