Files
yms-daemon/main.go
T

331 lines
10 KiB
Go
Raw Normal View History

2026-08-15 02:30:36 +08:00
package main
2026-08-16 01:27:30 +08:00
import (
"context"
2026-08-17 02:10:10 +08:00
"encoding/json"
2026-08-16 01:27:30 +08:00
"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()
2026-08-17 02:10:10 +08:00
if filepath.Base(os.Args[0]) == "ymsd" {
if len(os.Args) != 1 {
fmt.Fprintln(os.Stderr, "ymsd does not accept arguments")
os.Exit(2)
}
if err := runServe(ctx); err != nil {
fmt.Fprintln(os.Stderr, "ymsd failed:", err)
os.Exit(1)
}
return
}
2026-08-16 01:27:30 +08:00
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 "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 {
2026-08-17 02:10:10 +08:00
response, err = daemonclient.UpdateContainerImage(ctx, runtimepaths.Socket, request.service, request.imageReference, request.startLog, progress)
2026-08-16 17:12:06 +08:00
} 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 {
2026-08-17 02:10:10 +08:00
fmt.Fprintln(stderr, "ymsctl update failed:", err)
2026-08-16 01:27:30 +08:00
}
return 1
}
if !request.quite {
fmt.Fprintf(stdout, "transaction=%s state=%s\n", response.TransactionID, response.State)
}
return 0
2026-08-17 02:10:10 +08:00
case "list":
if err := runList(ctx, arguments[1:], stdout, stderr); err != nil {
fmt.Fprintln(stderr, "ymsctl list failed:", err)
return 1
}
return 0
2026-08-16 01:27:30 +08:00
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 {
2026-08-17 02:10:10 +08:00
fmt.Fprintln(stderr, "ymsctl restart failed:", err)
2026-08-16 01:27:30 +08:00
}
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-17 02:10:10 +08:00
startLog 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")
2026-08-17 02:10:10 +08:00
noStartLog := flags.Bool("no-start-log", false, "do not print container startup logs")
2026-08-16 01:27:30 +08:00
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 != "" {
2026-08-17 02:10:10 +08:00
return updateArguments{service: *service, inputType: daemonapi.InputTypeContainerImage, imageReference: *containerImage, quite: *quite, startLog: !*noStartLog}, nil
}
if *noStartLog {
return updateArguments{}, errors.New("--no-start-log requires --container-image")
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:")
2026-08-17 02:10:10 +08:00
fmt.Fprintln(output, " ymsctl update --service backend -f <repack.zip> [--quite]")
fmt.Fprintln(output, " ymsctl update --service backend --native-jar <backend.jar> [--quite]")
fmt.Fprintln(output, " ymsctl update --service backend --container-image <image-ref> [--no-start-log] [--quite]")
fmt.Fprintln(output, " ymsctl restart --service backend [--quite]")
fmt.Fprintln(output, " ymsctl list [--limit <n>] [--service backend] [--state <state>] [--json]")
2026-08-16 01:27:30 +08:00
}
func writeUpdateProgress(output io.Writer, event daemonapi.Response) {
state := event.State
if state == "" {
state = "INFO"
}
fmt.Fprintf(output, "%-14s %s\n", state, event.Message)
}
2026-08-17 02:10:10 +08:00
func runList(ctx context.Context, arguments []string, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("list", flag.ContinueOnError)
flags.SetOutput(stderr)
limit := flags.Int("limit", 20, "maximum number of operations to show")
service := flags.String("service", "", "exact service filter")
state := flags.String("state", "", "exact transaction state filter")
jsonOutput := flags.Bool("json", false, "write stable JSON")
if err := flags.Parse(arguments); err != nil {
return err
}
if flags.NArg() != 0 {
return errors.New("list does not accept positional arguments")
}
if *service != "" && *service != serviceBackend {
return errors.New("--service currently accepts only backend")
}
store, err := transaction.OpenStore(ctx, runtimepaths.Database)
if err != nil {
return err
}
defer store.Close()
items, err := store.ListRecent(ctx, transaction.ListFilter{Limit: *limit, Service: *service, State: transaction.State(*state)})
if err != nil {
return err
}
if *jsonOutput {
return json.NewEncoder(stdout).Encode(items)
}
fmt.Fprintln(stdout, "TIME\tTRANSACTION\tSERVICE\tSTATE\tSOURCE")
for _, item := range items {
fmt.Fprintf(stdout, "%s\t%s\t%s\t%s\t%s\n", item.CreatedAt.Local().Format("2006-01-02 15:04:05"), item.ID, item.Service, item.State, item.Source)
}
return nil
}