mirror of
https://github.com/datarhei/core.git
synced 2025-09-26 20:11:29 +08:00
216 lines
4.8 KiB
Go
216 lines
4.8 KiB
Go
package update
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"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()),
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
MaxIdleConns: 10,
|
|
IdleConnTimeout: 30 * time.Second,
|
|
},
|
|
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
|
|
}
|