mirror of
https://github.com/akyaiy/GoSally-mvp.git
synced 2026-03-04 11:12:25 +00:00
Implement automatic update functionality and improve server initialization; add NODE_PATH to Makefile, enhance logging, and update README
This commit is contained in:
@@ -43,18 +43,16 @@ type Internal struct {
|
||||
}
|
||||
|
||||
type Updates struct {
|
||||
UpdatesEnabled bool `yaml:"enabled" env-default:"false"`
|
||||
AllowAutoUpdates bool `yaml:"allow_auto_updates" env-default:"false"`
|
||||
AllowUpdates bool `yaml:"allow_updates" env-default:"false"`
|
||||
AllowDowngrades bool `yaml:"allow_downgrades" env-default:"false"`
|
||||
CheckInterval time.Duration `yaml:"check_interval" env-default:"2h"`
|
||||
RepositoryURL string `yaml:"repository_url" env-default:""`
|
||||
WantedVersion string `yaml:"wanted_version" env-default:"latest-stable"`
|
||||
UpdatesEnabled bool `yaml:"enabled" env-default:"false"`
|
||||
CheckInterval time.Duration `yaml:"check_interval" env-default:"2h"`
|
||||
RepositoryURL string `yaml:"repository_url" env-default:""`
|
||||
WantedVersion string `yaml:"wanted_version" env-default:"latest-stable"`
|
||||
}
|
||||
|
||||
// ConfigEnv structure for environment variables
|
||||
type ConfigEnv struct {
|
||||
ConfigPath string `env:"CONFIG_PATH" env-default:"./cfg/config.yaml"`
|
||||
NodePath string `env:"NODE_PATH" env-default:"./"`
|
||||
}
|
||||
|
||||
// MustLoadConfig loads the configuration from the specified path and environment variables.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
// UUIDLength is uuids length for sessions. By default it is 16 bytes.
|
||||
var UUIDLength byte = 4
|
||||
var UUIDLength byte = 16
|
||||
|
||||
// ApiRoute setting for go-chi for main route for api requests
|
||||
var ApiRoute string = "/api/{ver}"
|
||||
@@ -17,6 +19,12 @@ var NodeVersion string
|
||||
// In the repository, the file specified in the variable contains the current information about updates
|
||||
var ActualFileName string = "actual.txt"
|
||||
|
||||
// UpdateArchiveName is the name of the archive that will be used for updates.
|
||||
var UpdateArchiveName string = "gosally-node"
|
||||
|
||||
// UpdateInstallPath is the path where the update will be installed.
|
||||
var UpdateDownloadPath string = os.TempDir()
|
||||
|
||||
type _internalConsts struct{}
|
||||
type _serverConsts struct{}
|
||||
type _updateConsts struct{}
|
||||
@@ -28,7 +36,9 @@ func (_ _updateConsts) GetNodeVersion() string {
|
||||
}
|
||||
return NodeVersion
|
||||
}
|
||||
func (_ _updateConsts) GetActualFileName() string { return ActualFileName }
|
||||
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() byte { return UUIDLength }
|
||||
|
||||
45
core/init/init.go
Normal file
45
core/init/init.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if strings.HasPrefix(os.Args[0], "/tmp") {
|
||||
return
|
||||
}
|
||||
runPath, err := os.MkdirTemp("", "*-gs-runtime")
|
||||
log.SetOutput(os.Stderr)
|
||||
input, err := os.Open(os.Args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to init node: %s", err)
|
||||
}
|
||||
|
||||
runBinaryPath := filepath.Join(runPath, "node")
|
||||
output, err := os.Create(runBinaryPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to init node: %s", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
log.Fatalf("Failed to init node: %s", err)
|
||||
}
|
||||
|
||||
// Делаем исполняемым (на всякий случай)
|
||||
if err := os.Chmod(runBinaryPath, 0755); err != nil {
|
||||
log.Fatalf("Failed to init node: %s", err)
|
||||
}
|
||||
|
||||
input.Close()
|
||||
output.Close()
|
||||
runArgs := os.Args
|
||||
runArgs[0] = runBinaryPath
|
||||
if err := syscall.Exec(runBinaryPath, runArgs, append(os.Environ(), "GS_RUNTIME_PATH=" + runPath)); err != nil {
|
||||
log.Fatalf("Failed to init node: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -13,46 +13,46 @@ import (
|
||||
)
|
||||
|
||||
// HandlerV1 is the main handler for version 1 of the API.
|
||||
// The function processes the HTTP request and runs Lua scripts,
|
||||
// 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()
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID",
|
||||
slog.String("error", err.Error()))
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error())
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error())
|
||||
return
|
||||
}
|
||||
log := h.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", h.GetVersion()),
|
||||
slog.String("url", h.r.URL.String()),
|
||||
slog.String("method", h.r.Method),
|
||||
slog.String("url", r.URL.String()),
|
||||
slog.String("method", r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("connection-uuid", uuid16),
|
||||
slog.String("remote", h.r.RemoteAddr),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
log.Info("Received request")
|
||||
|
||||
cmd := chi.URLParam(h.r, "cmd")
|
||||
cmd := chi.URLParam(r, "cmd")
|
||||
if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) || !h.listAllowedCmd.MatchString(cmd) {
|
||||
log.Error("HTTP request error",
|
||||
slog.String("error", "invalid command"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid command")
|
||||
utils.WriteJSONError(w, http.StatusBadRequest, "invalid command")
|
||||
return
|
||||
}
|
||||
|
||||
scriptPath := h.comMatch(chi.URLParam(h.r, "ver"), cmd)
|
||||
scriptPath := h.comMatch(chi.URLParam(r, "ver"), cmd)
|
||||
if scriptPath == "" {
|
||||
log.Error("HTTP request error",
|
||||
slog.String("error", "command not found"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusNotFound))
|
||||
utils.WriteJSONError(h.w, http.StatusNotFound, "command not found")
|
||||
utils.WriteJSONError(w, http.StatusNotFound, "command not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
slog.String("error", "command not found"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusNotFound))
|
||||
utils.WriteJSONError(h.w, http.StatusNotFound, "command not found")
|
||||
utils.WriteJSONError(w, http.StatusNotFound, "command not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
defer L.Close()
|
||||
|
||||
paramsTable := L.NewTable()
|
||||
qt := h.r.URL.Query()
|
||||
qt := r.URL.Query()
|
||||
for k, v := range qt {
|
||||
if len(v) > 0 {
|
||||
L.SetField(paramsTable, k, lua.LString(v[0]))
|
||||
@@ -91,7 +91,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
if err := L.DoFile(prepareLuaEnv); err != nil {
|
||||
log.Error("Failed to prepare lua environment",
|
||||
slog.String("error", err.Error()))
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error())
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -101,7 +101,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
if err := L.DoFile(scriptPath); err != nil {
|
||||
log.Error("Failed to execute lua script",
|
||||
slog.String("error", err.Error()))
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error())
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
tbl, ok := lv.(*lua.LTable)
|
||||
if !ok {
|
||||
log.Error("Lua global 'Out' is not a table")
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "'Out' is not a table")
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "'Out' is not a table")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
resultTbl, ok := resultVal.(*lua.LTable)
|
||||
if !ok {
|
||||
log.Error("Lua global 'Result' is not a table")
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "'Result' is not a table")
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "'Result' is not a table")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
|
||||
})
|
||||
|
||||
h.w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(h.w).Encode(out); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||
log.Error("Failed to encode JSON response",
|
||||
slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
@@ -18,18 +18,18 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID",
|
||||
slog.String("error", err.Error()))
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error())
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error())
|
||||
return
|
||||
}
|
||||
log := h.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", h.GetVersion()),
|
||||
slog.String("url", h.r.URL.String()),
|
||||
slog.String("method", h.r.Method),
|
||||
slog.String("url", r.URL.String()),
|
||||
slog.String("method", r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("connection-uuid", uuid16),
|
||||
slog.String("remote", h.r.RemoteAddr),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
log.Info("Received request")
|
||||
@@ -46,11 +46,11 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
|
||||
log.Error("Failed to read commands directory",
|
||||
slog.String("error", err.Error()))
|
||||
utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
|
||||
utils.WriteJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
apiVer := chi.URLParam(h.r, "ver")
|
||||
apiVer := chi.URLParam(r, "ver")
|
||||
|
||||
// Сначала ищем версионные
|
||||
for _, file := range files {
|
||||
@@ -110,8 +110,8 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Info("Session completed")
|
||||
|
||||
h.w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(h.w).Encode(commands); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(commands); err != nil {
|
||||
h.log.Error("Failed to write JSON error response",
|
||||
slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package sv1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
@@ -21,9 +20,6 @@ type HandlerV1InitStruct struct {
|
||||
|
||||
// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests.
|
||||
type HandlerV1 struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
|
||||
log slog.Logger
|
||||
|
||||
cfg *config.ConfigConf
|
||||
|
||||
@@ -2,21 +2,18 @@ package sv1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
)
|
||||
|
||||
func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
|
||||
h.log.Error("HTTP request error",
|
||||
slog.String("remote", h.r.RemoteAddr),
|
||||
slog.String("method", h.r.Method),
|
||||
slog.String("url", h.r.URL.String()),
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
}
|
||||
// func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
// utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
|
||||
// h.log.Error("HTTP request error",
|
||||
// slog.String("remote", h.r.RemoteAddr),
|
||||
// slog.String("method", h.r.Method),
|
||||
// slog.String("url", h.r.URL.String()),
|
||||
// slog.Int("status", http.StatusBadRequest))
|
||||
// }
|
||||
|
||||
func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -187,15 +192,133 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, error) {
|
||||
if currentVersion == latestVersion && currentBranch == latestBranch {
|
||||
return false, nil
|
||||
}
|
||||
u.Log.Info("New update available",
|
||||
slog.String("current_version", string(currentVersion)),
|
||||
slog.String("current_branch", string(currentBranch)),
|
||||
slog.String("latest_version", string(latestVersion)),
|
||||
slog.String("latest_branch", string(latestBranch)),
|
||||
)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (u *Updater) Update() error {
|
||||
if !(u.Config.UpdatesEnabled) {
|
||||
return errors.New("updates are disabled in config, skipping update")
|
||||
}
|
||||
downloadPath, err := os.MkdirTemp("", "*-gs-up")
|
||||
if err != nil {
|
||||
return errors.New("failed to create temp dir " + err.Error())
|
||||
}
|
||||
//defer os.RemoveAll(downloadPath)
|
||||
_, currentBranch, err := u.GetCurrentVersion()
|
||||
if err != nil {
|
||||
return errors.New("failed to get current version: " + err.Error())
|
||||
}
|
||||
latestVersion, latestBranch, err := u.GetLatestVersion(currentBranch)
|
||||
if err != nil {
|
||||
return errors.New("failed to get latest version: " + err.Error())
|
||||
}
|
||||
updateArchiveName := config.GetUpdateConsts().GetUpdateArchiveName() + ".v" + string(latestVersion) + "-" + string(latestBranch)
|
||||
updateDest := 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())
|
||||
}
|
||||
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))
|
||||
}
|
||||
gzReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return errors.New("failed to create gzip reader: " + err.Error())
|
||||
}
|
||||
defer gzReader.Close()
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break // archive is fully read
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("failed to read tar header: " + err.Error())
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(downloadPath, header.Name)
|
||||
|
||||
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())
|
||||
}
|
||||
case tar.TypeReg:
|
||||
// Создаём директорию, если её ещё нет
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return errors.New("failed to create directory for file: " + err.Error())
|
||||
}
|
||||
// Создаём файл
|
||||
outFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return errors.New("failed to create file: " + err.Error())
|
||||
}
|
||||
// Копируем содержимое
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
outFile.Close()
|
||||
return errors.New("failed to copy file content: " + err.Error())
|
||||
}
|
||||
outFile.Close()
|
||||
default:
|
||||
return errors.New("unsupported tar entry type: " + string(header.Typeflag))
|
||||
}
|
||||
}
|
||||
return u.InstallAndRestart(filepath.Join(downloadPath, updateArchiveName, "node"))
|
||||
}
|
||||
|
||||
func (u *Updater) InstallAndRestart(newBinaryPath string) error {
|
||||
nodePath := os.Getenv("NODE_PATH")
|
||||
if nodePath == "" {
|
||||
return errors.New("NODE_PATH environment variable is not set")
|
||||
}
|
||||
installDir := filepath.Join(nodePath, "bin")
|
||||
targetPath := filepath.Join(installDir, "node")
|
||||
|
||||
// Копируем новый бинарник
|
||||
input, err := os.Open(newBinaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return 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()
|
||||
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 := syscall.Exec(targetPath, os.Args, os.Environ()); err != nil {
|
||||
// u.Log.Error("Failed to run new version automatickly", slog.String("err", err.Error()))
|
||||
// return err
|
||||
// }
|
||||
u.Log.Info("Shutting down")
|
||||
os.Exit(0)
|
||||
return errors.New("failed to shutdown the process")
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
Reference in New Issue
Block a user