diff --git a/cmd/root.go b/cmd/root.go index d8fd083..d330166 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,15 +1,18 @@ package cmd import ( - "fmt" "log" - "os" "get.pme.sh/pmesh/client" "get.pme.sh/pmesh/config" "get.pme.sh/pmesh/pmtp" + "get.pme.sh/pmesh/revision" + "get.pme.sh/pmesh/ui" + + "runtime" "github.com/spf13/cobra" + "go.uber.org/automaxprocs/maxprocs" ) var optURL = config.RootCommand.PersistentFlags().StringP( @@ -48,8 +51,12 @@ func getClientIf() client.Client { } func Execute() { + if runtime.GOMAXPROCS(0) > 32 { + runtime.GOMAXPROCS(32) + } + maxprocs.Set() + config.RootCommand.Short += ui.FaintStyle.Render(" (" + revision.GetVersion() + ")") if err := config.RootCommand.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) + ui.ExitWithError(err) } } diff --git a/cmd/start.go b/cmd/start.go index 7f89075..ac1981f 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -2,7 +2,6 @@ package cmd import ( "os" - "path/filepath" "strings" "get.pme.sh/pmesh/config" @@ -17,33 +16,6 @@ import ( ) func init() { - getManifestPath := func(args []string) string { - // Determine the path user wants to use for the manifest file - manifestPath := "" - if len(args) != 0 { - manifestPath = args[0] - } - - // If the path is empty, use the current working directory - if manifestPath == "" { - manifestPath = "." - } - - // If the path is not a valid yml file, try the default names - if !strings.HasSuffix(manifestPath, ".yml") || !strings.HasSuffix(manifestPath, ".yaml") { - yml := filepath.Join(manifestPath, "pm3.yml") - if _, err := os.Stat(yml); err == nil { - manifestPath = yml - } else { - manifestPath = filepath.Join(manifestPath, "pm3.yaml") - } - } - // Clean up the path and make it absolute if possible - if abs, err := filepath.Abs(manifestPath); err == nil { - manifestPath = abs - } - return filepath.Clean(manifestPath) - } config.RootCommand.AddCommand(&cobra.Command{ Use: "preview [manifest]", Short: "Previews the rendered manifest", @@ -51,7 +23,7 @@ func init() { GroupID: refGroup("daemon", "Daemon Commands"), RunE: func(cmd *cobra.Command, args []string) error { var node *yaml.Node - if err := lyml.Load(getManifestPath(args), &node); err != nil { + if err := lyml.Load(session.GetManifestPathFromArgs(args), &node); err != nil { return err } buf := &strings.Builder{} @@ -82,9 +54,8 @@ func init() { Args: cobra.MaximumNArgs(1), GroupID: refGroup("daemon", "Daemon Commands"), Run: func(cmd *cobra.Command, args []string) { - // Validate the options. setuputil.RunSetupIf(true) - session.Start(getManifestPath(args)) + session.Run(args) }, }) diff --git a/config/env.go b/config/env.go index 43f7fa8..b61df1a 100644 --- a/config/env.go +++ b/config/env.go @@ -74,7 +74,7 @@ func (s Subdir) File(name string) string { // Global from either environment or command line. var RootCommand = &cobra.Command{ Use: "pmesh", - Short: "pme.sh is an all-in one service manager, reverse proxy, and enterprise service bus", + Short: "pme.sh is an all-in one service manager, reverse proxy, and enterprise service bus.", PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { if cmd.Flag("cwd").Changed { err = os.Chdir(cmd.Flag("cwd").Value.String()) diff --git a/main.go b/main.go index 72ce9e4..5bbaba8 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,22 @@ package main import ( - "runtime" + "fmt" + "os" - "get.pme.sh/pmesh/cmd" + _ "embed" - "go.uber.org/automaxprocs/maxprocs" + "get.pme.sh/pmesh/cmd" + "get.pme.sh/pmesh/revision" ) func main() { - if runtime.GOMAXPROCS(0) > 32 { - runtime.GOMAXPROCS(32) + if len(os.Args) == 2 { + switch os.Args[1] { + case "--version", "-v", "version", "v", "ver": + fmt.Println(revision.GetVersion()) + os.Exit(0) + } } - maxprocs.Set() cmd.Execute() } diff --git a/revision/self.go b/revision/self.go new file mode 100644 index 0000000..33810c7 --- /dev/null +++ b/revision/self.go @@ -0,0 +1,28 @@ +package revision + +import ( + "fmt" + "runtime/debug" + "sync" +) + +var ( + VersionString = "v0.2" // Only updated for major/minor releases. +) + +func getCommit() string { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + if len(setting.Value) > 8 { + return setting.Value[:8] + } + } + } + } + return "00000000" +} + +var GetVersion = sync.OnceValue(func() string { + return fmt.Sprintf("%s-%s", VersionString, getCommit()) +}) diff --git a/session/session.go b/session/session.go index f5fa079..0e0b44c 100644 --- a/session/session.go +++ b/session/session.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "sync" "sync/atomic" "time" @@ -175,7 +176,6 @@ func (s *Session) StartService(name string, sv service.Service, invalidate bool) xlog.InfoC(ctx).Msg("Service started") return state, nil } - func (s *Session) Reload(invalidate bool) error { s.Lock() defer s.Unlock() @@ -295,25 +295,29 @@ func (s *Session) ReloadLocked(invalidate bool) error { return nil } -func (s *Session) Open() error { - xlog.Info().Stringer("id", s.ID).Msg("Session started") +func (s *Session) Open(ctx context.Context) error { + xlog.Info().Stringer("id", s.ID).Msg("Session starting") security.ObtainCertificate(config.Get().Secret) // Ensure the certificate is ready before starting the server - if err := s.Nats.Open(context.Background()); err != nil { - xlog.Err(err).Msg("Failed to open nats") - return err + + // Start the NATS gateway + if err := s.Nats.Open(ctx); err != nil { + return fmt.Errorf("failed to open nats: %w", err) } + // Start the peer list s.Peerlist = xpost.NewPeerlist(s.Nats) - if err := s.Peerlist.Open(s.Context); err != nil { - xlog.Err(err).Msg("Failed to open peer list") - return err + if err := s.Peerlist.Open(ctx); err != nil { + return fmt.Errorf("failed to open peer list: %w", err) } + + // Log the discovered peers if peers := s.Peerlist.List(true); len(peers) > 0 { for _, peer := range peers { xlog.Info().Str("host", peer.Host).Str("ip", peer.IP).Float64("lat", peer.Lat).Float64("lon", peer.Lon).Str("country", peer.Country).Str("isp", peer.ISP).Msg("Discovered peer") } } + // Add the SD source s.Peerlist.AddSDSource(func(out map[string]any) { out["commit"] = os.Getenv("PM3_COMMIT") out["branch"] = os.Getenv("PM3_BRANCH") @@ -337,14 +341,17 @@ func (s *Session) Open() error { out["services"] = healthyServices }) - err := s.Server.Listen() - if err != nil { - xlog.Err(err).Msg("Failed to start server") - return err + // Start the server + if err := s.Server.Listen(); err != nil { + return fmt.Errorf("failed to start server: %w", err) + } + + // Start the services + if err := s.Reload(false); err != nil { + return fmt.Errorf("failed to load manifest: %w", err) } return nil } - func (s *Session) Close() error { defer s.Cancel() s.ServiceMap.Range(func(name string, sv *ServiceState) bool { @@ -392,7 +399,7 @@ func (s *Session) Shutdown(ctx context.Context) { } } -func Start(manifestPath string) { +func OpenAndServe(manifestPath string) { xlog.Info().Str("manifest", manifestPath).Str("host", config.Get().Host).Msg("Starting node") s, err := New(manifestPath) @@ -406,12 +413,8 @@ func Start(manifestPath string) { defer cancel() s.Shutdown(ctx) }() - if err = s.Open(); err != nil { - xlog.Err(err).Msg("Failed to open session") - return - } - if err = s.Reload(false); err != nil { - xlog.Err(err).Msg("Failed to load manifest") + if err = s.Open(context.Background()); err != nil { + xlog.Err(err).Msg("Failed to initialize session") return } @@ -420,3 +423,35 @@ func Start(manifestPath string) { case <-s.Context.Done(): } } + +func GetManifestPathFromArgs(args []string) string { + // Determine the path user wants to use for the manifest file + manifestPath := "" + if len(args) != 0 { + manifestPath = args[0] + } + + // If the path is empty, use the current working directory + if manifestPath == "" { + manifestPath = "." + } + + // If the path is not a valid yml file, try the default names + if !strings.HasSuffix(manifestPath, ".yml") || !strings.HasSuffix(manifestPath, ".yaml") { + yml := filepath.Join(manifestPath, "pm3.yml") + if _, err := os.Stat(yml); err == nil { + manifestPath = yml + } else { + manifestPath = filepath.Join(manifestPath, "pm3.yaml") + } + } + // Clean up the path and make it absolute if possible + if abs, err := filepath.Abs(manifestPath); err == nil { + manifestPath = abs + } + return filepath.Clean(manifestPath) +} +func Run(args []string) { + manifestPath := GetManifestPathFromArgs(args) + OpenAndServe(manifestPath) +} diff --git a/ui/styles.go b/ui/styles.go index 8b0e592..e74685f 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "get.pme.sh/pmesh/config" "github.com/charmbracelet/lipgloss" ) @@ -34,7 +35,11 @@ func RenderErrorLine(err any) string { return errLinePfx + Display(err) } func ExitWithError(err any) { - fmt.Println(RenderErrorLine(err) + "\n") + if *config.Dumb { + fmt.Fprintf(os.Stderr, "Fatal error: %v", err) + } else { + fmt.Println(RenderErrorLine(err) + "\n") + } os.Exit(1) } diff --git a/xpost/peerlist.go b/xpost/peerlist.go index a0a4976..ed243bc 100644 --- a/xpost/peerlist.go +++ b/xpost/peerlist.go @@ -170,7 +170,7 @@ func (m *Peerlist) Open(ctx context.Context) error { m.self = self m.last, m.err = m.update(ctx, self) if m.err == nil { - ctx, m.cancel = context.WithCancel(ctx) + ctx, m.cancel = context.WithCancel(context.Background()) go m.tick(ctx, self) } return m.err