Fixing updates

This commit is contained in:
alex
2025-07-10 18:03:30 +03:00
parent e71d69f3f1
commit 5bc334fd2c
21 changed files with 617 additions and 418 deletions

View File

@@ -2,8 +2,11 @@ package config
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -52,12 +55,6 @@ func (c *Compositor) LoadConf(path string) error {
v.SetDefault("updates.check_interval", "2h")
v.SetDefault("updates.wanted_version", "latest-stable")
// поддержка ENV-переопределений
v.SetEnvPrefix("GOSALLY")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// читаем YAML
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("error reading config: %w", err)
}
@@ -67,6 +64,116 @@ func (c *Compositor) LoadConf(path string) error {
return fmt.Errorf("error unmarshaling config: %w", err)
}
c.Conf = &Conf{}
c.Conf = &cfg
return nil
}
func (c *Compositor) LoadCMDLine(root *cobra.Command) {
cmdLine := &CMDLine{}
c.CMDLine = cmdLine
t := reflect.TypeOf(cmdLine).Elem()
v := reflect.ValueOf(cmdLine).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldVal := v.Field(i)
ptr := fieldVal.Addr().Interface()
use := strings.ToLower(field.Name)
var cmd *cobra.Command
for _, sub := range root.Commands() {
if sub.Use == use {
cmd = sub
break
}
}
if use == root.Use {
cmd = root
}
if cmd == nil {
continue
}
Unmarshal(cmd, ptr)
}
}
func Unmarshal(cmd *cobra.Command, target any) {
t := reflect.TypeOf(target).Elem()
v := reflect.ValueOf(target).Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
valPtr := v.Field(i).Addr().Interface()
full := field.Tag.Get("full")
short := field.Tag.Get("short")
def := field.Tag.Get("def")
desc := field.Tag.Get("desc")
isPersistent := field.Tag.Get("persistent") == "true"
flagSet := cmd.Flags()
if isPersistent {
flagSet = cmd.PersistentFlags()
}
switch field.Type.Kind() {
case reflect.String:
flagSet.StringVarP(valPtr.(*string), full, short, def, desc)
case reflect.Bool:
defVal, err := strconv.ParseBool(def)
if err != nil && def != "" {
fmt.Printf("warning: cannot parse default bool: %q\n", def)
}
flagSet.BoolVarP(valPtr.(*bool), full, short, defVal, desc)
case reflect.Int:
defVal, err := strconv.Atoi(def)
if err != nil && def != "" {
fmt.Printf("warning: cannot parse default int: %q\n", def)
}
flagSet.IntVarP(valPtr.(*int), full, short, defVal, desc)
case reflect.Slice:
elemKind := field.Type.Elem().Kind()
switch elemKind {
case reflect.String:
defVals := []string{}
if def != "" {
defVals = strings.Split(def, ",")
}
flagSet.StringSliceVarP(valPtr.(*[]string), full, short, defVals, desc)
case reflect.Int:
var intVals []int
if def != "" {
for _, s := range strings.Split(def, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
n, err := strconv.Atoi(s)
if err != nil {
fmt.Printf("warning: cannot parse int in slice: %q\n", s)
continue
}
intVals = append(intVals, n)
}
}
flagSet.IntSliceVarP(valPtr.(*[]int), full, short, intVals, desc)
default:
fmt.Printf("unsupported slice element type: %s\n", elemKind)
}
default:
fmt.Printf("unsupported field type: %s\n", field.Type.Kind())
}
}
}

View File

@@ -6,16 +6,15 @@ import (
"time"
)
var ConfigPath string
type CompositorContract interface {
LoadEnv() error
LoadConf(path string) error
}
type Compositor struct {
Conf *Conf
Env *Env
CMDLine *CMDLine
Conf *Conf
Env *Env
}
type Conf struct {
@@ -57,3 +56,17 @@ type Env struct {
NodePath string `mapstructure:"node_path"`
ParentStagePID int `mapstructure:"parent_pid"`
}
type CMDLine struct {
Run Run
Node Root
}
type Root struct {
Debug bool `persistent:"true" full:"debug" short:"d" def:"false" desc:"Set debug mode"`
}
type Run struct {
ConfigPath string `persistent:"true" full:"config" short:"c" def:"./config.yaml" desc:"Path to configuration file"`
Test []int `persistent:"true" full:"test" short:"t" def:"" desc:"js test"`
}

View File

@@ -27,25 +27,8 @@ var UpdateDownloadPath string = os.TempDir()
var MetaDir string = "./.meta"
type _internalConsts struct{}
type _serverConsts struct{}
type _updateConsts struct{}
func GetUpdateConsts() _updateConsts { return _updateConsts{} }
func (_ _updateConsts) GetNodeVersion() string {
func init() {
if NodeVersion == "" {
return "v0.0.0-none"
NodeVersion = "v0.0.0-none"
}
return NodeVersion
}
func (_ _updateConsts) GetActualFileName() string { return ActualFileName }
func (_ _updateConsts) GetUpdateArchiveName() string { return UpdateArchiveName }
func (_ _updateConsts) GetUpdateDownloadPath() string { return UpdateDownloadPath }
func GetInternalConsts() _internalConsts { return _internalConsts{} }
func (_ _internalConsts) GetUUIDLength() int { return UUIDLength }
func (_ _internalConsts) GetMetaDir() string { return MetaDir }
func GetServerConsts() _serverConsts { return _serverConsts{} }
func (_ _serverConsts) GetApiRoute() string { return ApiRoute }
func (_ _serverConsts) GetComDirRoute() string { return ComDirRoute }

View File

@@ -28,7 +28,7 @@ func readNodeUUIDRaw(p string) ([]byte, error) {
if err != nil {
return data, err
}
if len(data) != config.GetInternalConsts().GetUUIDLength() {
if len(data) != config.UUIDLength {
return data, errors.New("decoded UUID length mismatch")
}
return data, nil

View File

@@ -1,115 +0,0 @@
package corestate
import (
"fmt"
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/core/utils"
)
func NewRM() *RunManager {
return &RunManager{
indexedPaths: func() *map[string]string { m := make(map[string]string); return &m }(),
created: false,
}
}
func (c *CoreState) RuntimeDir() RunManagerContract {
return c.RM
}
// Create creates a temp directory
func (r *RunManager) Create(uuid32 string) (string, error) {
if r.created {
return r.runDir, fmt.Errorf("runtime directory is already created")
}
path, err := os.MkdirTemp("", fmt.Sprintf("*-%s-%s", uuid32, "gosally-runtime"))
if err != nil {
return "", err
}
r.runDir = path
r.created = true
return path, nil
}
func (r *RunManager) Clean() error {
return utils.CleanTempRuntimes(r.runDir)
}
// Quite dangerous and goofy.
// TODO: implement a better variant of runDir indexing on the second stage of initialization
func (r *RunManager) Toggle() string {
r.runDir = filepath.Dir(os.Args[0])
r.created = true
return r.runDir
}
func (r *RunManager) Get(index string) (string, error) {
if !r.created {
return "", fmt.Errorf("runtime directory is not created")
}
if r.indexedPaths == nil {
err := r.indexPaths()
if err != nil {
return "", nil
}
}
if r.indexedPaths == nil {
return "", fmt.Errorf("indexedPaths is nil")
}
value, ok := (*r.indexedPaths)[index]
if !ok {
err := r.indexPaths()
if err != nil {
return "", err
}
value, ok = (*r.indexedPaths)[index]
if !ok {
return "", fmt.Errorf("cannot detect file under index %s", index)
}
}
return value, nil
}
func (r *RunManager) Set(index string) error {
if !r.created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(r.runDir, index)
dir := filepath.Dir(fullPath)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if r.indexedPaths == nil {
err = r.indexPaths()
if err != nil {
return err
}
} else {
(*r.indexedPaths)[index] = fullPath
}
return nil
}
func (r *RunManager) indexPaths() error {
if !r.created {
return fmt.Errorf("runtime directory is not created")
}
i, err := utils.IndexPaths(r.runDir)
if err != nil {
return err
}
r.indexedPaths = i
return nil
}

View File

@@ -1,16 +1,10 @@
package corestate
import (
"context"
"os"
)
// CoreStateContract is interface for CoreState.
// CoreState is a structure that contains the basic meta-information vital to the node.
// The interface contains functionality for working with the Runtime directory and its files,
// and access to low-level logging in stdout
type CoreStateContract interface {
RuntimeDir() RunManagerContract
}
type CoreState struct {
@@ -27,36 +21,4 @@ type CoreState struct {
NodePath string
MetaDir string
RunDir string
RM *RunManager
}
type RunManagerContract interface {
Get(index string) (string, error)
// Set recursively creates a file in runDir
Set(index string) error
File(index string) RunFileManagerContract
indexPaths() error
}
type RunManager struct {
created bool
runDir string
// I obviously keep it with a pointer because it makes me feel calmer
indexedPaths *map[string]string
}
type RunFileManagerContract interface {
Open() (*os.File, error)
Close() error
Watch(ctx context.Context, callback func()) error
}
type RunFileManager struct {
err error
indexedPath string
file *os.File
}

View File

@@ -1,4 +1,4 @@
package corestate
package run_manager
import (
"context"
@@ -8,16 +8,16 @@ import (
"time"
)
func (r *RunManager) File(index string) RunFileManagerContract {
value, ok := (*r.indexedPaths)[index]
func File(index string) RunFileManagerContract {
value, ok := indexedPaths[index]
if !ok {
err := r.indexPaths()
err := indexPaths()
if err != nil {
return &RunFileManager{
err: err,
}
}
value, ok = (*r.indexedPaths)[index]
value, ok = indexedPaths[index]
if !ok {
return &RunFileManager{
err: fmt.Errorf("cannot detect file under index %s", index),
@@ -45,22 +45,24 @@ func (r *RunFileManager) Close() error {
return r.file.Close()
}
func (r *RunFileManager) Watch(ctx context.Context, callback func()) error {
func (r *RunFileManager) Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error) {
if r.err != nil {
return r.err
return nil, r.err
}
if r.file == nil {
return fmt.Errorf("file is not opened")
return nil, fmt.Errorf("file is not opened")
}
info, err := r.file.Stat()
if err != nil {
return err
return nil, err
}
origStat := info.Sys().(*syscall.Stat_t)
origIno := origStat.Ino
origModTime := info.ModTime()
ctx, cancel := context.WithCancel(parentCtx)
go func() {
for {
select {
@@ -89,5 +91,5 @@ func (r *RunFileManager) Watch(ctx context.Context, callback func()) error {
}
}()
return nil
return cancel, nil
}

View File

@@ -0,0 +1,158 @@
package run_manager
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/core/utils"
)
type RunManagerContract interface {
Get(index string) (string, error)
// Set recursively creates a file in runDir
Set(index string) error
File(index string) RunFileManagerContract
indexPaths() error
}
var (
created bool
runDir string
indexedPaths = make(map[string]string)
)
type RunFileManagerContract interface {
Open() (*os.File, error)
Close() error
Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error)
}
type RunFileManager struct {
err error
indexedPath string
file *os.File
}
// func (c *CoreState) RuntimeDir() RunManagerContract {
// return c.RM
// }
// Create creates a temp directory
func Create(uuid32 string) (string, error) {
if created {
return runDir, fmt.Errorf("runtime directory is already created")
}
path, err := os.MkdirTemp("", fmt.Sprintf("*-%s-%s", uuid32, "gosally-runtime"))
if err != nil {
return "", err
}
runDir = path
created = true
return path, nil
}
func Clean() error {
created = false
indexedPaths = nil
return utils.CleanTempRuntimes(runDir)
}
// Quite dangerous and goofy.
// TODO: implement a better variant of runDir indexing on the second stage of initialization
func Toggle() string {
runDir = filepath.Dir(os.Args[0])
created = true
return runDir
}
func Get(index string) (string, error) {
if !created {
return "", fmt.Errorf("runtime directory is not created")
}
if indexedPaths == nil {
err := indexPaths()
if err != nil {
return "", nil
}
}
if indexedPaths == nil {
return "", fmt.Errorf("indexedPaths is nil")
}
value, ok := indexedPaths[index]
if !ok {
err := indexPaths()
if err != nil {
return "", err
}
value, ok = indexedPaths[index]
if !ok {
return "", fmt.Errorf("cannot detect file under index %s", index)
}
}
return value, nil
}
func Set(index string) error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(runDir, index)
dir := filepath.Dir(fullPath)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if indexedPaths == nil {
err = indexPaths()
if err != nil {
return err
}
} else {
indexedPaths[index] = fullPath
}
return nil
}
func SetDir(index string) error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(runDir, index)
err := os.MkdirAll(fullPath, 0755)
if err != nil {
return err
}
return nil
}
func indexPaths() error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
i, err := utils.IndexPaths(runDir)
if err != nil {
return err
}
indexedPaths = i
return nil
}
func RuntimeDir() string {
return runDir
}

View File

@@ -18,7 +18,7 @@ import (
// The function processes the HTTP request and runs Lua scripts,
// preparing the environment and subsequently transmitting the execution result
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID(int(config.GetInternalConsts().GetUUIDLength()))
uuid16, err := utils.NewUUID(int(config.UUIDLength))
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
@@ -127,7 +127,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
})
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.GetInternalConsts().GetMetaDir(), "uuid"))
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: cmd,

View File

@@ -16,7 +16,7 @@ import (
// The function processes the HTTP request and returns a list of available commands.
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID(int(config.GetInternalConsts().GetUUIDLength()))
uuid16, err := utils.NewUUID(int(config.UUIDLength))
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
@@ -111,7 +111,7 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
log.Debug("Command list prepared")
log.Info("Session completed")
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.GetInternalConsts().GetMetaDir(), "uuid"))
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: "list",

View File

@@ -6,16 +6,18 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/run_manager"
"github.com/akyaiy/GoSally-mvp/core/utils"
"golang.org/x/net/context"
)
const (
@@ -36,14 +38,20 @@ type UpdaterContract interface {
}
type Updater struct {
Log slog.Logger
Config *config.Conf
log *log.Logger
config *config.Conf
env *config.Env
ctx context.Context
cancel context.CancelFunc
}
func NewUpdater(log slog.Logger, cfg *config.Conf) *Updater {
func NewUpdater(ctx context.Context, log *log.Logger, cfg *config.Conf, env *config.Env) *Updater {
return &Updater{
Log: log,
Config: cfg,
log: log,
config: cfg,
env: env,
ctx: ctx,
}
}
@@ -80,11 +88,11 @@ func isVersionNewer(current, latest Version) bool {
if i < len(currentParts) {
cur, err := strconv.Atoi(currentParts[i])
if err != nil {
cur = 0 // или можно обработать ошибку иначе
cur = 0
}
curPart = cur
} else {
curPart = 0 // Если части в current меньше, считаем недостающие нулями
curPart = 0
}
if i < len(latestParts) {
@@ -103,31 +111,15 @@ func isVersionNewer(current, latest Version) bool {
if curPart > latPart {
return false
}
// если равны — идём дальше
}
return false // все части равны, значит не новее
return false
}
// if len(currentParts) >= 1 && len(latestParts) >= 1 {
// if currentParts[0] < latestParts[0] {
// if len(currentParts) < 2 || len(latestParts) < 2 {
// if currentParts[1] < latestParts[1] {
// return true
// }
// if currentParts[1] > latestParts[1] {
// return false
// }
// }
// if currentParts[0] > latestParts[0] {
// return false
// }
// }
// GetCurrentVersion reads the current version from the version file and returns it along with the branch.
func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
version, branch, err := splitVersionString(string(config.GetUpdateConsts().GetNodeVersion()))
version, branch, err := splitVersionString(string(config.NodeVersion))
if err != nil {
u.Log.Error("Failed to parse version string", slog.String("version", string(config.GetUpdateConsts().GetNodeVersion())), slog.String("error", err.Error()))
u.log.Printf("Failed to parse version string: %s", err.Error())
return "", "", err
}
switch branch {
@@ -139,28 +131,28 @@ func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
}
func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error) {
repoURL := u.Config.Updates.RepositoryURL
repoURL := u.config.Updates.RepositoryURL
if repoURL == "" {
u.Log.Error("RepositoryURL is empty in config")
u.log.Printf("Failed to get latest version: %s", "RepositoryURL is empty in config")
return "", "", errors.New("repository URL is empty")
}
if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") {
u.Log.Error("RepositoryURL does not start with http:// or https://", slog.String("RepositoryURL", repoURL))
u.log.Printf("Failed to get latest version: %s: %s", "RepositoryURL does not start with http:// or https:/", repoURL)
return "", "", errors.New("repository URL must start with http:// or https://")
}
response, err := http.Get(repoURL + "/" + config.GetUpdateConsts().GetActualFileName())
response, err := http.Get(repoURL + "/" + config.ActualFileName)
if err != nil {
u.Log.Error("Failed to fetch latest version", slog.String("error", err.Error()))
u.log.Printf("Failed to fetch latest version: %s", err.Error())
return "", "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
u.Log.Error("Failed to fetch latest version", slog.Int("status", response.StatusCode))
u.log.Printf("Failed to fetch latest version: HTTP status %d", response.StatusCode)
return "", "", errors.New("failed to fetch latest version, status code: " + http.StatusText(response.StatusCode))
}
data, err := io.ReadAll(response.Body)
if err != nil {
u.Log.Error("Failed to read latest version response", slog.String("error", err.Error()))
u.log.Printf("Failed to read latest version response: %s", err.Error())
return "", "", err
}
lines := strings.Split(string(data), "\n")
@@ -171,14 +163,13 @@ func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error)
}
version, branch, err := splitVersionString(string(line))
if err != nil {
u.Log.Error("Failed to parse version string", slog.String("version", string(line)), slog.String("error", err.Error()))
u.log.Printf("Failed to parse version string: %s", err.Error())
return "", "", err
}
if branch == updateBranch {
return Version(version), Branch(branch), nil
}
}
u.Log.Warn("No version found for branch", slog.String("branch", string(updateBranch)))
return "", "", errors.New("no version found for branch: " + string(updateBranch))
}
@@ -198,146 +189,145 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, error) {
}
func (u *Updater) Update() error {
if !(u.Config.Updates.UpdatesEnabled) {
if !u.config.Updates.UpdatesEnabled {
return errors.New("updates are disabled in config, skipping update")
}
downloadPath, err := os.MkdirTemp("", "*-gosally-update")
if err != nil {
return errors.New("failed to create temp dir " + err.Error())
if err := run_manager.SetDir("update"); err != nil {
return fmt.Errorf("failed to create update dir: %w", err)
}
downloadPath := filepath.Join(run_manager.RuntimeDir(), "update")
_, currentBranch, err := u.GetCurrentVersion()
if err != nil {
return errors.New("failed to get current version: " + err.Error())
return fmt.Errorf("failed to get current version: %w", err)
}
latestVersion, latestBranch, err := u.GetLatestVersion(currentBranch)
if err != nil {
return errors.New("failed to get latest version: " + err.Error())
return fmt.Errorf("failed to get latest version: %w", err)
}
updateArchiveName := config.GetUpdateConsts().GetUpdateArchiveName() + ".v" + string(latestVersion) + "-" + string(latestBranch)
updateDest := u.Config.Updates.RepositoryURL + "/" + updateArchiveName + ".tar.gz"
updateArchiveName := fmt.Sprintf("%s.v%s-%s", config.UpdateArchiveName, latestVersion, latestBranch)
updateDest := fmt.Sprintf("%s/%s.%s", u.config.Updates.RepositoryURL, updateArchiveName, "tar.gz")
resp, err := http.Get(updateDest)
if err != nil {
return errors.New("failed to fetch latest version archive: " + err.Error())
return fmt.Errorf("failed to fetch archive: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return errors.New("failed to fetch latest version archive: status " + resp.Status + ", body: " + string(body))
return fmt.Errorf("unexpected HTTP status: %s, body: %s", resp.Status, body)
}
gzReader, err := gzip.NewReader(resp.Body)
if err != nil {
return errors.New("failed to create gzip reader: " + err.Error())
return fmt.Errorf("gzip reader error: %w", err)
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break // archive is fully read
break
}
if err != nil {
return errors.New("failed to read tar header: " + err.Error())
return fmt.Errorf("tar read error: %w", err)
}
targetPath := filepath.Join(downloadPath, header.Name)
relativeParts := strings.SplitN(header.Name, string(os.PathSeparator), 2)
if len(relativeParts) < 2 {
// It's either a top level directory or garbage.
continue
}
cleanName := relativeParts[1]
targetPath := filepath.Join(downloadPath, cleanName)
switch header.Typeflag {
case tar.TypeDir:
// Создаём директорию
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return errors.New("failed to create directory: " + err.Error())
return fmt.Errorf("mkdir error: %w", err)
}
case tar.TypeReg:
// Создаём директорию, если её ещё нет
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return errors.New("failed to create directory for file: " + err.Error())
if err := run_manager.Set(filepath.Join("update", cleanName)); err != nil {
return fmt.Errorf("set file error: %w", err)
}
// Создаём файл
outFile, err := os.Create(targetPath)
f := run_manager.File(filepath.Join("update", cleanName))
outFile, err := f.Open()
if err != nil {
return errors.New("failed to create file: " + err.Error())
return fmt.Errorf("open file error: %w", err)
}
// Копируем содержимое
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return errors.New("failed to copy file content: " + err.Error())
return fmt.Errorf("copy file error: %w", err)
}
outFile.Close()
default:
return errors.New("unsupported tar entry type: " + string(header.Typeflag))
return fmt.Errorf("unsupported tar type: %v", header.Typeflag)
}
}
return u.InstallAndRestart(filepath.Join(downloadPath, updateArchiveName, "node"))
return u.InstallAndRestart()
}
func (u *Updater) InstallAndRestart(newBinaryPath string) error {
nodePath := os.Getenv("NODE_PATH")
func (u *Updater) InstallAndRestart() error {
nodePath := u.env.NodePath
if nodePath == "" {
return errors.New("NODE_PATH environment variable is not set")
return errors.New("GS_NODE_PATH environment variable is not set")
}
installDir := filepath.Join(nodePath, "bin")
targetPath := filepath.Join(installDir, "node")
// Копируем новый бинарник
input, err := os.Open(newBinaryPath)
f := run_manager.File("update/node")
input, err := f.Open()
if err != nil {
return err
return fmt.Errorf("cannot open new binary: %w", err)
}
defer f.Close()
output, err := os.Create(targetPath)
if err != nil {
return err
return fmt.Errorf("cannot create target binary: %w", err)
}
if _, err := io.Copy(output, input); err != nil {
return err
}
if err := os.Chmod(targetPath, 0755); err != nil {
return errors.New("failed to chmod file: " + err.Error())
}
input.Close()
reSafeTmpDir := regexp.QuoteMeta(os.TempDir())
toClean := regexp.MustCompile(
fmt.Sprintf(`^(%s/\d+-gosally-update/)`, reSafeTmpDir),
).FindStringSubmatch(newBinaryPath)
if len(toClean) > 1 {
os.RemoveAll(toClean[0])
output.Close()
return fmt.Errorf("copy failed: %w", err)
}
output.Close()
// Запускаем новый процесс
u.Log.Info("Launching new version...", slog.String("path", targetPath))
cmd := exec.Command(targetPath, os.Args[1:]...)
cmd.Env = os.Environ()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = nil
if err = cmd.Start(); err != nil {
return err
if err := os.Chmod(targetPath, 0755); err != nil {
return fmt.Errorf("failed to chmod: %w", err)
}
u.Log.Info("Shutting down")
os.Exit(0)
return errors.New("failed to shutdown the process")
u.log.Printf("Launching new version: path is %s", targetPath)
// cmd := exec.Command(targetPath, os.Args[1:]...)
// cmd.Env = os.Environ()
// cmd.Stdout = os.Stdout
// cmd.Stderr = os.Stderr
// cmd.Stdin = os.Stdin
args := os.Args
args[0] = targetPath
env := utils.SetEviron(os.Environ(), "GS_PARENT_PID=-1")
run_manager.Clean()
return syscall.Exec(targetPath, args, env)
//u.cancel()
// TODO: fix this crap and find a better way to update without errors
// for {
// _, err := run_manager.Get("run.lock")
// if err != nil {
// break
// }
// }
// return cmd.Start()
}
// func (u *Updater) Update() error {
// if !(u.Config.UpdatesEnabled && u.Config.Updates.AllowUpdates && u.Config.Updates.AllowDowngrades) {
// u.Log.Info("Updates are disabled in config, skipping update")
// return nil
// }
// wantedVersion := u.Config.Updates.WantedVersion
// _, wantedBranch, _ := splitVersionString(wantedVersion)
// newVersion, newBranch, err := u.GetLatestVersion(wantedBranch)
// if err != nil {
// return err
// }
// if wantedBranch != newBranch {
// u.Log.Info("Wanted version branch does not match latest version branch: updating wanted branch",
// slog.String("wanted_branch", string(wantedBranch)),
// slog.String("latest_branch", string(newBranch)),
// )
// }
// }
func (u *Updater) Shutdownfunc(f context.CancelFunc) {
u.cancel = f
}

View File

@@ -1,11 +1,34 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
)
func SetEviron(eviron []string, envs ...string) []string {
envMap := make(map[string]string)
for _, e := range eviron {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
for _, e := range envs {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
newEviron := make([]string, 0, len(envMap))
for k, v := range envMap {
newEviron = append(newEviron, fmt.Sprintf("%s=%s", k, v))
}
return newEviron
}
func CleanTempRuntimes(pattern string) error {
matches, err := filepath.Glob(pattern)
if err != nil {
@@ -42,7 +65,7 @@ func ExistsMatchingDirs(pattern, exclude string) (bool, error) {
return false, nil
}
func IndexPaths(runDir string) (*map[string]string, error) {
func IndexPaths(runDir string) (map[string]string, error) {
indexed := make(map[string]string)
err := filepath.Walk(runDir, func(path string, info os.FileInfo, err error) error {
@@ -67,7 +90,7 @@ func IndexPaths(runDir string) (*map[string]string, error) {
return nil, err
}
return &indexed, nil
return indexed, nil
}
func IsFullyInitialized(i any) bool {

48
core/utils/utils_test.go Normal file
View File

@@ -0,0 +1,48 @@
package utils
import (
"fmt"
"reflect"
"sort"
"testing"
)
func TestFunc_SetEviron(t *testing.T) {
tests := []struct {
eviron []string
envs []string
want []string
}{
{
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
[]string{"ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
{
[]string{"ENV1=1", "ENV2=5", "ENV3=4"},
[]string{"ENV2=2", "ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
{
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
[]string{"ENV4=4"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3", "ENV4=4"},
},
{
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
[]string{"ENV3=2", "ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("in %q set new %q", tt.eviron, tt.envs), func(t *testing.T) {
got := SetEviron(tt.eviron, tt.envs...)
sort.Strings(got)
sort.Strings(tt.want)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SetEviron(%q, %q) = got %v; want %v", tt.eviron, tt.envs, got, tt.want)
}
})
}
}

View File

@@ -26,15 +26,15 @@ func NewUUID(length int) (string, error) {
}
func NewUUID32() (string, error) {
return NewUUID(config.GetInternalConsts().GetUUIDLength())
return NewUUID(config.UUIDLength)
}
func NewUUID32Raw() ([]byte, error) {
data, err := NewUUIDRaw(config.GetInternalConsts().GetUUIDLength())
data, err := NewUUIDRaw(config.UUIDLength)
if err != nil {
return data, err
}
if len(data) != config.GetInternalConsts().GetUUIDLength() {
if len(data) != config.UUIDLength {
return data, errors.New("unexpected UUID length")
}
return data, nil