mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-05 00:53:58 -04:00
231 lines
5.2 KiB
Go
231 lines
5.2 KiB
Go
package installer
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
resultFile = "result.json"
|
|
)
|
|
|
|
type Result struct {
|
|
Success bool
|
|
Error string
|
|
ExecutedAt time.Time
|
|
}
|
|
|
|
// ResultHandler handles reading and writing update results
|
|
type ResultHandler struct {
|
|
resultFile string
|
|
}
|
|
|
|
// NewResultHandler creates a new communicator with the given directory path
|
|
// The result file will be created as "result.json" in the specified directory
|
|
func NewResultHandler(installerDir string) *ResultHandler {
|
|
// Create it if it doesn't exist
|
|
// do not care if already exists
|
|
_ = os.MkdirAll(installerDir, 0o700)
|
|
|
|
rh := &ResultHandler{
|
|
resultFile: filepath.Join(installerDir, resultFile),
|
|
}
|
|
return rh
|
|
}
|
|
|
|
func (rh *ResultHandler) GetErrorResultReason() string {
|
|
result, err := rh.tryReadResult()
|
|
if err == nil && !result.Success {
|
|
return result.Error
|
|
}
|
|
|
|
if err := rh.cleanup(); err != nil {
|
|
log.Warnf("failed to cleanup result file: %v", err)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (rh *ResultHandler) WriteSuccess() error {
|
|
result := Result{
|
|
Success: true,
|
|
ExecutedAt: time.Now(),
|
|
}
|
|
return rh.write(result)
|
|
}
|
|
|
|
func (rh *ResultHandler) WriteErr(errReason error) error {
|
|
result := Result{
|
|
Success: false,
|
|
Error: errReason.Error(),
|
|
ExecutedAt: time.Now(),
|
|
}
|
|
return rh.write(result)
|
|
}
|
|
|
|
func (rh *ResultHandler) Watch(ctx context.Context) (Result, error) {
|
|
log.Infof("start watching result: %s", rh.resultFile)
|
|
|
|
// Check if file already exists (updater finished before we started watching)
|
|
if result, err := rh.tryReadResult(); err == nil {
|
|
log.Infof("installer result: %v", result)
|
|
return result, nil
|
|
}
|
|
|
|
dir := filepath.Dir(rh.resultFile)
|
|
|
|
if err := rh.waitForDirectory(ctx, dir); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
return rh.watchForResultFile(ctx, dir)
|
|
}
|
|
|
|
func (rh *ResultHandler) waitForDirectory(ctx context.Context, dir string) error {
|
|
ticker := time.NewTicker(300 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rh *ResultHandler) watchForResultFile(ctx context.Context, dir string) (Result, error) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return Result{}, err
|
|
}
|
|
|
|
defer func() {
|
|
if err := watcher.Close(); err != nil {
|
|
log.Warnf("failed to close watcher: %v", err)
|
|
}
|
|
}()
|
|
|
|
if err := watcher.Add(dir); err != nil {
|
|
return Result{}, fmt.Errorf("failed to watch directory: %v", err)
|
|
}
|
|
|
|
// Check again after setting up watcher to avoid race condition
|
|
// (file could have been created between initial check and watcher setup)
|
|
if result, err := rh.tryReadResult(); err == nil {
|
|
log.Infof("installer result: %v", result)
|
|
return result, nil
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return Result{}, ctx.Err()
|
|
case event, ok := <-watcher.Events:
|
|
if !ok {
|
|
return Result{}, errors.New("watcher closed unexpectedly")
|
|
}
|
|
|
|
if result, done := rh.handleWatchEvent(event); done {
|
|
return result, nil
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return Result{}, errors.New("watcher closed unexpectedly")
|
|
}
|
|
return Result{}, fmt.Errorf("watcher error: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rh *ResultHandler) handleWatchEvent(event fsnotify.Event) (Result, bool) {
|
|
if event.Name != rh.resultFile {
|
|
return Result{}, false
|
|
}
|
|
|
|
if event.Has(fsnotify.Create) {
|
|
result, err := rh.tryReadResult()
|
|
if err != nil {
|
|
log.Debugf("error while reading result: %v", err)
|
|
return result, true
|
|
}
|
|
log.Infof("installer result: %v", result)
|
|
return result, true
|
|
}
|
|
|
|
return Result{}, false
|
|
}
|
|
|
|
// Write writes the update result to a file for the UI to read
|
|
func (rh *ResultHandler) write(result Result) error {
|
|
log.Infof("write out installer result to: %s", rh.resultFile)
|
|
// Ensure directory exists
|
|
dir := filepath.Dir(rh.resultFile)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
log.Errorf("failed to create directory %s: %v", dir, err)
|
|
return err
|
|
}
|
|
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write to a temporary file first, then rename for atomic operation
|
|
tmpPath := rh.resultFile + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
|
|
log.Errorf("failed to create temp file: %s", err)
|
|
return err
|
|
}
|
|
|
|
// Atomic rename
|
|
if err := os.Rename(tmpPath, rh.resultFile); err != nil {
|
|
if cleanupErr := os.Remove(tmpPath); cleanupErr != nil {
|
|
log.Warnf("Failed to remove temp result file: %v", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rh *ResultHandler) cleanup() error {
|
|
err := os.Remove(rh.resultFile)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
log.Debugf("delete installer result file: %s", rh.resultFile)
|
|
return nil
|
|
}
|
|
|
|
// tryReadResult attempts to read and validate the result file
|
|
func (rh *ResultHandler) tryReadResult() (Result, error) {
|
|
data, err := os.ReadFile(rh.resultFile)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
var result Result
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return Result{}, fmt.Errorf("invalid result format: %w", err)
|
|
}
|
|
|
|
if err := rh.cleanup(); err != nil {
|
|
log.Warnf("failed to cleanup result file: %v", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|