virtual-vxlan/updater.go

273 lines
6.2 KiB
Go
Raw Normal View History

2025-01-05 22:22:24 -06:00
package main
import (
"bufio"
"context"
"errors"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/coreos/go-systemd/daemon"
"github.com/creativeprojects/go-selfupdate"
"github.com/hashicorp/go-version"
log "github.com/sirupsen/logrus"
)
// Check for update and update if one is available.
func Update(c *UpdateConfig) error {
log.Println("Checking for update.")
// Setup source.
source, err := selfupdate.NewGitHubSource(selfupdate.GitHubConfig{})
if err != nil {
return err
}
// Get the path to ourself.
exe, err := selfupdate.ExecutablePath()
if err != nil {
return fmt.Errorf("could not locate executable path: %s", err)
}
updateDir, cmd := filepath.Split(exe)
oldSavePath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", cmd))
// Get updater with source and validator.
updater, err := selfupdate.NewUpdater(selfupdate.Config{
Source: source,
Validator: &selfupdate.ChecksumValidator{UniqueFilename: "checksums.txt"},
OldSavePath: oldSavePath,
})
if err != nil {
return err
}
// Find the latest release.
release, found, err := updater.DetectLatest(context.Background(), selfupdate.NewRepositorySlug(c.Owner, c.Repo))
if err != nil {
return err
}
if !found {
log.Println("No updates available.")
return nil
}
// Compare the versions.
thisVersion, err := version.NewVersion(c.CurrentVersion)
if err != nil {
return err
}
latestVersion, err := version.NewVersion(release.Version())
if err != nil {
return err
}
// If an update isn't available, end.
if !thisVersion.LessThan(latestVersion) {
log.Println("No updates available.")
return nil
}
log.Println("Updating to version:", release.Version())
// We're updating, tell the app so services can be stopped.
c.PreUpdate()
// Perform the update.
err = updater.UpdateTo(context.Background(), release, exe)
// If update failed, rollback and tell the app we failed.
abortUpdate:
if err != nil {
rerr := os.Rename(oldSavePath, exe)
if rerr != nil {
log.Println("Failed to rollback update:", rerr)
}
c.AbortUpdate()
return err
}
log.Println("Updated.")
// If relaunch is requested, then start the new exe and confirm it works.
if c.ShouldRelaunch {
// Make process and get its stdout/stderr.
log.Println("Starting new process.")
p := exec.Command(exe, os.Args[1:]...)
p.Env = os.Environ()
p.Env = append(p.Env, "UPDATER_UPDATE=1")
stdout, err := p.StdoutPipe()
if err != nil {
goto abortUpdate
}
stderr, err := p.StderrPipe()
if err != nil {
goto abortUpdate
}
// Start process.
err = p.Start()
if err != nil {
goto abortUpdate
}
// Set timeout to kill process.
timer := time.AfterFunc(c.StartupTimeout, func() {
p.Process.Kill()
})
// Channels to confirm success or failure.
success := make(chan bool)
failure := make(chan error)
// Scan the stdout for success message.
go func() {
stdoutScanner := bufio.NewScanner(stdout)
for stdoutScanner.Scan() {
line := stdoutScanner.Text()
fmt.Fprintln(os.Stdout, line)
if c.IsSuccessMsg(line) {
success <- true
return
}
}
}()
// Scan the stderr for sucess message.
go func() {
stderrScanner := bufio.NewScanner(stderr)
for stderrScanner.Scan() {
line := stderrScanner.Text()
fmt.Fprintln(os.Stderr, line)
if c.IsSuccessMsg(line) {
success <- true
return
}
}
}()
// Wait for command exit, and pass errors.
go func() {
err := p.Wait()
failure <- err
}()
// Wait for one of the channels to be sent information.
select {
case <-success:
case err = <-failure:
if err == nil {
err = errors.New("program exited without error")
}
}
// Stop the timer as the process is either going to be left running or is stopped.
timer.Stop()
// If stop due to error, abort.
if err != nil {
goto abortUpdate
}
// The update was successful, so we can remove the old binary.
os.Remove(oldSavePath)
// Set the new process stdio to ours.
p.Stdout = os.Stdout
p.Stderr = os.Stderr
p.Stdin = os.Stdin
// Notify systemd to watch the new process.
// Those babysitting fees are too high to have it watch us too.
// Amy said SystemD doesn't do babysitting.
daemon.SdNotify(false, fmt.Sprintf("MAINPID=%d", p.Process.Pid))
// Quit this process as the new process will continue running as a fork.
os.Exit(0)
}
// The update was successful, so we can remove the old binary.
os.Remove(oldSavePath)
return nil
}
// Check for updates, and apply.
func CheckForUpdate(c *UpdateConfig, relaunch bool) {
// Set update config local variables.
c.CurrentVersion = serviceVersion
c.ShouldRelaunch = relaunch
c.PreUpdate = func() {
// If no app defined, stop here.
if app == nil {
return
}
2025-01-05 22:22:24 -06:00
// Stop all listeners to allow updated service to start.
for len(app.Net.Listeners) >= 1 {
app.Net.Listeners[0].Close()
}
// Stop the grpc server.
if app.grpcServer != nil {
app.grpcServer.Close()
}
}
c.IsSuccessMsg = func(msg string) bool {
if strings.Contains(msg, "Service started.") {
return true
}
return false
}
c.StartupTimeout = 5 * time.Minute
// If update is aborted, we should restart the service.
c.AbortUpdate = func() {
// If no app defined, stop here.
if app == nil {
return
}
2025-01-05 22:22:24 -06:00
// Read the configuration from file.
config := ReadConfig()
// Start the GRPC server for cli communication.
_, err := NewGRPCServer(config.RPCPath)
if err != nil {
log.Fatalln(err)
}
// Apply the configuration read.
err = ApplyConfig(config)
// If error applying the config, we should fail.
if err != nil {
log.Fatalln(err)
}
}
err := Update(c)
if err != nil {
log.Println("Failure checking for update:", err)
}
}
// Every 24 hours, check for updates.
func (a *App) RunUpdateLoop() {
// If disabled, don't run loop.
if app.UpdateConfig.Disabled {
return
}
// Randomly check for updates at first start.
if os.Getenv("UPDATER_UPDATE") != "1" && rand.Intn(20) == 2 {
CheckForUpdate(app.UpdateConfig, true)
}
// Run update check every 24 hours.
for {
nextUpdate := time.Hour * 24
nextUpdate += time.Duration(rand.Intn(18000)) * time.Second
time.Sleep(nextUpdate)
CheckForUpdate(app.UpdateConfig, true)
}
}