mirror of
				https://github.com/datarhei/core.git
				synced 2025-10-31 11:26:52 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			217 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			217 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package update
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"regexp"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/datarhei/core/v16/encoding/json"
 | |
| 	"github.com/datarhei/core/v16/log"
 | |
| 	"github.com/datarhei/core/v16/monitor/metric"
 | |
| 	"golang.org/x/mod/semver"
 | |
| )
 | |
| 
 | |
| // Config is the configuration for the update check
 | |
| type Config struct {
 | |
| 	ID      string
 | |
| 	Name    string
 | |
| 	Version string
 | |
| 	Arch    string
 | |
| 	Monitor metric.Reader
 | |
| 	Logger  log.Logger
 | |
| }
 | |
| 
 | |
| // UpdateCheck is an interface
 | |
| type Checker interface {
 | |
| 	Start()
 | |
| 	Stop()
 | |
| }
 | |
| 
 | |
| type checker struct {
 | |
| 	id      string
 | |
| 	name    string
 | |
| 	version string
 | |
| 	arch    string
 | |
| 	monitor metric.Reader
 | |
| 
 | |
| 	startOnce sync.Once
 | |
| 	stopOnce  sync.Once
 | |
| 
 | |
| 	stopTicker context.CancelFunc
 | |
| 
 | |
| 	logger log.Logger
 | |
| }
 | |
| 
 | |
| // New creates a new service instance that implements the Service interface
 | |
| func New(config Config) (Checker, error) {
 | |
| 	s := &checker{
 | |
| 		id:      config.ID,
 | |
| 		name:    config.Name,
 | |
| 		version: "v" + config.Version,
 | |
| 		arch:    config.Arch,
 | |
| 		monitor: config.Monitor,
 | |
| 		logger:  config.Logger,
 | |
| 	}
 | |
| 
 | |
| 	if s.logger == nil {
 | |
| 		s.logger = log.New("")
 | |
| 	}
 | |
| 
 | |
| 	if s.monitor == nil {
 | |
| 		return nil, fmt.Errorf("no monitor provided")
 | |
| 	}
 | |
| 
 | |
| 	// drain stop once, so it can't be called before startOnce has been called
 | |
| 	s.stopOnce.Do(func() {})
 | |
| 
 | |
| 	return s, nil
 | |
| }
 | |
| 
 | |
| func (s *checker) tick(ctx context.Context, interval, delay time.Duration) {
 | |
| 	time.Sleep(delay)
 | |
| 
 | |
| 	err := s.check()
 | |
| 	if err != nil {
 | |
| 		s.logger.WithError(err).Warn().Log("Failed to check for updates")
 | |
| 	}
 | |
| 
 | |
| 	ticker := time.NewTicker(interval)
 | |
| 	defer ticker.Stop()
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-ctx.Done():
 | |
| 			return
 | |
| 		case <-ticker.C:
 | |
| 			err := s.check()
 | |
| 			if err != nil {
 | |
| 				s.logger.WithError(err).Warn().Log("Failed to check for updates")
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *checker) Start() {
 | |
| 	s.startOnce.Do(func() {
 | |
| 		ctx, cancel := context.WithCancel(context.Background())
 | |
| 		s.stopTicker = cancel
 | |
| 		go s.tick(ctx, 24*time.Hour, 10*time.Second)
 | |
| 
 | |
| 		s.stopOnce = sync.Once{}
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (s *checker) Stop() {
 | |
| 	s.stopOnce.Do(func() {
 | |
| 		s.stopTicker()
 | |
| 		s.startOnce = sync.Once{}
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type checkRequest struct {
 | |
| 	AppVersion         string `json:"app_version"`
 | |
| 	CoreID             string `json:"core_id"`
 | |
| 	CoreArch           string `json:"core_aarch"`
 | |
| 	CoreUptimeSeconds  uint64 `json:"core_uptime_seconds"`
 | |
| 	CoreProcessRunning uint64 `json:"core_process_running"`
 | |
| 	CoreProcessFailed  uint64 `json:"core_process_failed"`
 | |
| 	CoreProcessKilled  uint64 `json:"core_process_killed"`
 | |
| 	CoreViewer         uint64 `json:"core_viewer"`
 | |
| }
 | |
| 
 | |
| type checkResponse struct {
 | |
| 	LatestVersion string `json:"latest_version"`
 | |
| }
 | |
| 
 | |
| func (s *checker) check() error {
 | |
| 	metrics := s.monitor.Collect([]metric.Pattern{
 | |
| 		metric.NewPattern("uptime_uptime"),
 | |
| 		metric.NewPattern("ffmpeg_process"),
 | |
| 		metric.NewPattern("restream_state"),
 | |
| 		metric.NewPattern("session_active"),
 | |
| 	})
 | |
| 
 | |
| 	request := checkRequest{
 | |
| 		AppVersion:         s.name + " " + s.version,
 | |
| 		CoreID:             s.id,
 | |
| 		CoreArch:           s.arch,
 | |
| 		CoreUptimeSeconds:  uint64(metrics.Value("uptime_uptime").Val()),
 | |
| 		CoreProcessRunning: uint64(metrics.Value("restream_state", "state", "running").Val()),
 | |
| 		CoreProcessFailed:  uint64(metrics.Value("ffmpeg_process", "state", "failed").Val()),
 | |
| 		CoreProcessKilled:  uint64(metrics.Value("ffmpeg_process", "state", "killed").Val()),
 | |
| 		CoreViewer:         uint64(metrics.Value("session_active", "collector", "hls").Val() + metrics.Value("session_active", "collector", "rtmp").Val()),
 | |
| 	}
 | |
| 
 | |
| 	tr := http.DefaultTransport.(*http.Transport).Clone()
 | |
| 	tr.MaxIdleConns = 10
 | |
| 	tr.IdleConnTimeout = 30 * time.Second
 | |
| 
 | |
| 	client := &http.Client{
 | |
| 		Transport: tr,
 | |
| 		Timeout:   5 * time.Second,
 | |
| 	}
 | |
| 
 | |
| 	var data bytes.Buffer
 | |
| 	encoder := json.NewEncoder(&data)
 | |
| 	if err := encoder.Encode(&request); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	s.logger.Debug().WithField("request", data.String()).Log("")
 | |
| 
 | |
| 	req, err := http.NewRequest(http.MethodPut, "https://service.datarhei.com/api/v1/app_version", &data)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	req.Header.Add("Content-Type", "application/json")
 | |
| 
 | |
| 	res, err := client.Do(req)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if res.StatusCode != 200 {
 | |
| 		return fmt.Errorf("request failed: %s", http.StatusText(res.StatusCode))
 | |
| 	}
 | |
| 
 | |
| 	body, err := io.ReadAll(res.Body)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("error reading response: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	response := checkResponse{}
 | |
| 
 | |
| 	if err := json.Unmarshal(body, &response); err != nil {
 | |
| 		return fmt.Errorf("error parsing response: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	re := regexp.MustCompile(`\s(v\d+\.\d+\.\d+)\s?`)
 | |
| 	matches := re.FindStringSubmatch(response.LatestVersion)
 | |
| 	if matches == nil {
 | |
| 		return fmt.Errorf("no version information detected in response")
 | |
| 	}
 | |
| 
 | |
| 	cmp := semver.Compare(matches[1], s.version)
 | |
| 
 | |
| 	s.logger.Debug().WithFields(log.Fields{
 | |
| 		"comparison": cmp,
 | |
| 		"current":    s.version,
 | |
| 		"available":  matches[1],
 | |
| 	}).Log("")
 | |
| 
 | |
| 	if cmp == 1 {
 | |
| 		s.logger.Info().WithFields(log.Fields{
 | |
| 			"current":   s.version,
 | |
| 			"available": matches[1],
 | |
| 		}).Log("New version available")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | 
