2026-08-15 02:30:36 +08:00
|
|
|
package main
|
|
|
|
|
|
2026-08-16 01:27:30 +08:00
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"yms-daemon/internal/backendupdate"
|
2026-08-16 17:12:06 +08:00
|
|
|
"yms-daemon/internal/containerengine"
|
2026-08-16 01:27:30 +08:00
|
|
|
"yms-daemon/internal/daemonapi"
|
|
|
|
|
"yms-daemon/internal/daemonclient"
|
|
|
|
|
"yms-daemon/internal/daemonserver"
|
|
|
|
|
"yms-daemon/internal/deploymentconfig"
|
|
|
|
|
"yms-daemon/internal/filestore"
|
|
|
|
|
"yms-daemon/internal/hostnginx"
|
|
|
|
|
"yms-daemon/internal/logging"
|
|
|
|
|
"yms-daemon/internal/processlock"
|
|
|
|
|
"yms-daemon/internal/runtimepaths"
|
|
|
|
|
"yms-daemon/internal/systemd"
|
|
|
|
|
"yms-daemon/internal/transaction"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const serviceBackend = "backend"
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
|
|
|
defer cancel()
|
|
|
|
|
os.Exit(run(ctx, os.Args[1:], os.Stdout, os.Stderr))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func run(ctx context.Context, arguments []string, stdout io.Writer, stderr io.Writer) int {
|
|
|
|
|
if len(arguments) == 0 {
|
|
|
|
|
writeUsage(stderr)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
switch arguments[0] {
|
|
|
|
|
case "serve":
|
|
|
|
|
if len(arguments) != 1 {
|
|
|
|
|
fmt.Fprintln(stderr, "serve does not accept arguments")
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
if err := runServe(ctx); err != nil {
|
|
|
|
|
fmt.Fprintln(stderr, "yms-daemon serve failed:", err)
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
case "update":
|
|
|
|
|
request, err := parseUpdateArgs(arguments[1:], stderr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(stderr, err)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
var progress func(daemonapi.Response)
|
|
|
|
|
if !request.quite {
|
|
|
|
|
progress = func(event daemonapi.Response) {
|
|
|
|
|
writeUpdateProgress(stdout, event)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-16 17:12:06 +08:00
|
|
|
var response daemonapi.Response
|
|
|
|
|
if request.inputType == daemonapi.InputTypeContainerImage {
|
|
|
|
|
response, err = daemonclient.UpdateContainerImage(ctx, runtimepaths.Socket, request.service, request.imageReference, progress)
|
|
|
|
|
} else {
|
|
|
|
|
response, err = daemonclient.Update(ctx, runtimepaths.Socket, request.service, request.inputType, request.file, progress)
|
|
|
|
|
}
|
2026-08-16 01:27:30 +08:00
|
|
|
if err != nil {
|
|
|
|
|
if response.TransactionID != "" {
|
|
|
|
|
fmt.Fprintf(stderr, "transaction=%s state=%s error=%v\n", response.TransactionID, response.State, err)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Fprintln(stderr, "yms-daemon update failed:", err)
|
|
|
|
|
}
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
if !request.quite {
|
|
|
|
|
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
case "restart":
|
|
|
|
|
request, err := parseRestartArgs(arguments[1:], stderr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintln(stderr, err)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
var progress func(daemonapi.Response)
|
|
|
|
|
if !request.quite {
|
|
|
|
|
progress = func(event daemonapi.Response) {
|
|
|
|
|
writeUpdateProgress(stdout, event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
response, err := daemonclient.Restart(ctx, runtimepaths.Socket, request.service, progress)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if response.TransactionID != "" {
|
|
|
|
|
fmt.Fprintf(stderr, "transaction=%s state=%s error=%v\n", response.TransactionID, response.State, err)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Fprintln(stderr, "yms-daemon restart failed:", err)
|
|
|
|
|
}
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
if !request.quite {
|
|
|
|
|
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
case "help", "-h", "--help":
|
|
|
|
|
writeUsage(stdout)
|
|
|
|
|
return 0
|
|
|
|
|
default:
|
|
|
|
|
fmt.Fprintf(stderr, "unknown command: %s\n", arguments[0])
|
|
|
|
|
writeUsage(stderr)
|
|
|
|
|
return 2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type updateArguments struct {
|
2026-08-16 17:12:06 +08:00
|
|
|
service string
|
|
|
|
|
inputType string
|
|
|
|
|
file string
|
|
|
|
|
imageReference string
|
|
|
|
|
quite bool
|
2026-08-16 01:27:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type restartArguments struct {
|
|
|
|
|
service string
|
|
|
|
|
quite bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseUpdateArgs(arguments []string, output io.Writer) (updateArguments, error) {
|
|
|
|
|
flags := flag.NewFlagSet("update", flag.ContinueOnError)
|
|
|
|
|
flags.SetOutput(output)
|
|
|
|
|
service := flags.String("service", "", "service to update")
|
|
|
|
|
file := flags.String("f", "", "repack ZIP path")
|
|
|
|
|
nativeJAR := flags.String("native-jar", "", "direct native backend JAR path")
|
2026-08-16 17:12:06 +08:00
|
|
|
containerImage := flags.String("container-image", "", "development backend container image reference")
|
2026-08-16 01:27:30 +08:00
|
|
|
quite := flags.Bool("quite", false, "suppress progress and successful result output")
|
|
|
|
|
if err := flags.Parse(arguments); err != nil {
|
|
|
|
|
return updateArguments{}, err
|
|
|
|
|
}
|
|
|
|
|
if flags.NArg() != 0 {
|
|
|
|
|
return updateArguments{}, errors.New("update does not accept positional arguments")
|
|
|
|
|
}
|
|
|
|
|
if *service != serviceBackend {
|
|
|
|
|
return updateArguments{}, errors.New("--service currently accepts only backend")
|
|
|
|
|
}
|
2026-08-16 17:12:06 +08:00
|
|
|
inputCount := 0
|
|
|
|
|
for _, value := range []string{*file, *nativeJAR, *containerImage} {
|
|
|
|
|
if value != "" {
|
|
|
|
|
inputCount++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if inputCount != 1 {
|
|
|
|
|
return updateArguments{}, errors.New("exactly one of -f, --native-jar, and --container-image is required; update inputs are mutually exclusive")
|
|
|
|
|
}
|
|
|
|
|
if *containerImage != "" {
|
|
|
|
|
return updateArguments{service: *service, inputType: daemonapi.InputTypeContainerImage, imageReference: *containerImage, quite: *quite}, nil
|
2026-08-16 01:27:30 +08:00
|
|
|
}
|
|
|
|
|
inputType := daemonapi.InputTypeRepackZIP
|
|
|
|
|
inputFile := *file
|
|
|
|
|
if *nativeJAR != "" {
|
|
|
|
|
inputType = daemonapi.InputTypeNativeJAR
|
|
|
|
|
inputFile = *nativeJAR
|
|
|
|
|
}
|
|
|
|
|
absoluteFile, err := filepath.Abs(inputFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return updateArguments{}, fmt.Errorf("resolve update package path: %w", err)
|
|
|
|
|
}
|
|
|
|
|
info, err := os.Lstat(absoluteFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return updateArguments{}, fmt.Errorf("inspect update package %s: %w", absoluteFile, err)
|
|
|
|
|
}
|
|
|
|
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
|
|
|
|
return updateArguments{}, fmt.Errorf("update package is not a direct regular file: %s", absoluteFile)
|
|
|
|
|
}
|
|
|
|
|
return updateArguments{service: *service, inputType: inputType, file: absoluteFile, quite: *quite}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseRestartArgs(arguments []string, output io.Writer) (restartArguments, error) {
|
|
|
|
|
flags := flag.NewFlagSet("restart", flag.ContinueOnError)
|
|
|
|
|
flags.SetOutput(output)
|
|
|
|
|
service := flags.String("service", "", "service to restart")
|
|
|
|
|
quite := flags.Bool("quite", false, "suppress progress and successful result output")
|
|
|
|
|
if err := flags.Parse(arguments); err != nil {
|
|
|
|
|
return restartArguments{}, err
|
|
|
|
|
}
|
|
|
|
|
if flags.NArg() != 0 {
|
|
|
|
|
return restartArguments{}, errors.New("restart does not accept positional arguments")
|
|
|
|
|
}
|
|
|
|
|
if *service != serviceBackend {
|
|
|
|
|
return restartArguments{}, errors.New("--service currently accepts only backend")
|
|
|
|
|
}
|
|
|
|
|
return restartArguments{service: *service, quite: *quite}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runServe(ctx context.Context) (result error) {
|
|
|
|
|
logger, logCloser, err := logging.New(runtimepaths.Log)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer func() { result = errors.Join(result, logCloser.Close()) }()
|
|
|
|
|
|
|
|
|
|
lock, err := processlock.Acquire(runtimepaths.Lock)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer func() { result = errors.Join(result, lock.Close()) }()
|
|
|
|
|
|
|
|
|
|
config, err := deploymentconfig.Load(deploymentconfig.DefaultPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
store, err := transaction.OpenStore(ctx, runtimepaths.Database)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer func() { result = errors.Join(result, store.Close()) }()
|
|
|
|
|
coordinator, err := transaction.NewCoordinator(store, logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
gateway, err := hostnginx.NewController(
|
|
|
|
|
runtimepaths.HostNginxConfig,
|
|
|
|
|
runtimepaths.HostNginxExecutable,
|
|
|
|
|
config.Backend.SystemctlPath,
|
|
|
|
|
runtimepaths.HostNginxService,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
httpClient := &http.Client{Timeout: 5 * time.Second}
|
2026-08-16 17:12:06 +08:00
|
|
|
var updater *backendupdate.Updater
|
|
|
|
|
switch config.Backend.Type {
|
|
|
|
|
case deploymentconfig.BackendTypeNative:
|
|
|
|
|
releaseStore, err := filestore.New(config.Backend.ReleaseDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
units, err := systemd.NewSystemctl(config.Backend.SystemctlPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
updater, err = backendupdate.New(config, runtimepaths.WorkRoot, store, coordinator, releaseStore, units, gateway, httpClient, logger)
|
|
|
|
|
case deploymentconfig.BackendTypeContainer:
|
|
|
|
|
var engine *containerengine.MobyEngine
|
|
|
|
|
engine, err = containerengine.NewMobyEngine()
|
|
|
|
|
if err == nil {
|
|
|
|
|
defer func() { result = errors.Join(result, engine.Close()) }()
|
|
|
|
|
updater, err = backendupdate.NewContainer(config, runtimepaths.WorkRoot, store, coordinator, engine, gateway, httpClient, logger)
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
err = fmt.Errorf("unsupported backend.type %q", config.Backend.Type)
|
|
|
|
|
}
|
2026-08-16 01:27:30 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
server, err := daemonserver.New(runtimepaths.Socket, updater, logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return server.Serve(ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeUsage(output io.Writer) {
|
|
|
|
|
fmt.Fprintln(output, "usage:")
|
|
|
|
|
fmt.Fprintln(output, " yms-daemon serve")
|
|
|
|
|
fmt.Fprintln(output, " yms-daemon update --service backend -f <repack.zip> [--quite]")
|
|
|
|
|
fmt.Fprintln(output, " yms-daemon update --service backend --native-jar <backend.jar> [--quite]")
|
2026-08-16 17:12:06 +08:00
|
|
|
fmt.Fprintln(output, " yms-daemon update --service backend --container-image <image-ref> [--quite]")
|
2026-08-16 01:27:30 +08:00
|
|
|
fmt.Fprintln(output, " yms-daemon restart --service backend [--quite]")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeUpdateProgress(output io.Writer, event daemonapi.Response) {
|
|
|
|
|
state := event.State
|
|
|
|
|
if state == "" {
|
|
|
|
|
state = "INFO"
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(output, "%-14s %s\n", state, event.Message)
|
|
|
|
|
}
|