package main import ( "context" "errors" "flag" "fmt" "io" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "yms-daemon/internal/backendupdate" "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) } } response, err := daemonclient.Update(ctx, runtimepaths.Socket, request.service, request.inputType, request.file, 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 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 { service string inputType string file string quite bool } 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") 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") } if (*file == "") == (*nativeJAR == "") { return updateArguments{}, errors.New("exactly one of -f and --native-jar is required") } 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 } releaseStore, err := filestore.New(config.Backend.ReleaseDir) if err != nil { return err } units, err := systemd.NewSystemctl(config.Backend.SystemctlPath) 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} updater, err := backendupdate.New(config, runtimepaths.WorkRoot, store, coordinator, releaseStore, units, gateway, httpClient, logger) 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 [--quite]") fmt.Fprintln(output, " yms-daemon update --service backend --native-jar [--quite]") 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) }