package ignitecmd import ( "context" "io" "os" "os/signal" "syscall" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" "golang.org/x/term" cmdmodel "git.cw.tr/mukan-network/mukan-ignite/ignite/cmd/bubblemodel" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui" uilog "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/log" cliuimodel "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/model" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/events" "git.cw.tr/mukan-network/mukan-ignite/ignite/services/chain" ) const ( flagVerbose = "verbose" flagConfig = "config" flagForceReset = "force-reset" flagGenerateClients = "generate-clients" flagQuitOnFail = "quit-on-fail" flagResetOnce = "reset-once" flagOutputFile = "output-file" ) var isTerminal = term.IsTerminal // NewChainServe creates a new serve command to serve a blockchain. func NewChainServe() *cobra.Command { c := &cobra.Command{ Use: "serve", Short: "Start a blockchain node in development", Long: `The serve command compiles and installs the binary (like "ignite chain build"), uses that binary to initialize the blockchain's data directory for one validator (like "ignite chain init"), and starts the node locally for development purposes with automatic code reloading. Automatic code reloading means Ignite starts watching the project directory. Whenever a file change is detected, Ignite automatically rebuilds, reinitializes and restarts the node. Whenever possible Ignite will try to keep the current state of the chain by exporting and importing the genesis file. To force Ignite to start from a clean slate even if a genesis file exists, use the following flag: ignite chain serve --reset-once To force Ignite to reset the state every time the source code is modified, use the following flag: ignite chain serve --force-reset With Ignite it's possible to start more than one blockchain from the same source code using different config files. This is handy if you're building inter-blockchain functionality and, for example, want to try sending packets from one blockchain to another. To start a node using a specific config file: ignite chain serve --config mars.yml The serve command is meant to be used ONLY FOR DEVELOPMENT PURPOSES. Under the hood, it runs "appd start", where "appd" is the name of your chain's binary. For production, you may want to run "appd start" manually. `, Args: cobra.NoArgs, RunE: chainServeHandler, } flagSetPath(c) flagSetClearCache(c) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetCheckDependencies()) c.Flags().AddFlagSet(flagSetSkipProto()) c.Flags().AddFlagSet(flagSetSkipBuild()) c.Flags().AddFlagSet(flagSetVerbose()) c.Flags().BoolP(flagForceReset, "f", false, "force reset of the app state on start and every source change") c.Flags().BoolP(flagResetOnce, "r", false, "reset the app state once on init") c.Flags().Bool(flagGenerateClients, false, "generate code for the configured clients on reset or source code change") c.Flags().Bool(flagQuitOnFail, false, "quit program if the app fails to start") c.Flags().StringSlice(flagBuildTags, []string{}, "parameters to build the chain binary") c.Flags().StringP(flagOutputFile, "o", "", "output file logging the chain output (no UI, no stdin, listens for SIGTERM, implies --yes) (default: stdout)") return c } func chainServeHandler(cmd *cobra.Command, _ []string) error { if shouldRunServeInDaemonMode(cmd) { return daemonMode(cmd) } options := []cliui.Option{cliui.WithoutUserInteraction(getYes(cmd))} // Session must not handle events when the verbosity is the default // to allow render of the UI and events using bubbletea. The custom // UI is not used for other verbosity levels in which the session // must handle the events to use custom output prefixes. verbosity := getVerbosity(cmd) if verbosity == uilog.VerbosityDefault { options = append(options, cliui.IgnoreEvents()) } else { options = append(options, cliui.WithVerbosity(verbosity)) } session := cliui.New(options...) defer session.End() // Depending on the verbosity execute the serve command within // a bubbletea context to display the custom UI. if verbosity == uilog.VerbosityDefault { bus := session.EventBus() bus.Send("Initializing...", events.ProgressStart()) // Render UI m := cmdmodel.NewChainServe(cmd, bus, chainServeCmd(cmd, session)) _, err := tea.NewProgram(m, tea.WithInput(cmd.InOrStdin())).Run() return err } // Otherwise run the serve command directly return chainServe(cmd, session) } func shouldRunServeInDaemonMode(cmd *cobra.Command) bool { if cmd.Flags().Changed(flagOutputFile) { return true } return !hasTerminalInputAndOutput(cmd.InOrStdin(), cmd.OutOrStdout()) } func hasTerminalInputAndOutput(stdin io.Reader, stdout io.Writer) bool { stdinFD, ok := fileDescriptor(stdin) if !ok { return false } stdoutFD, ok := fileDescriptor(stdout) if !ok { return false } return isTerminal(stdinFD) && isTerminal(stdoutFD) } type fileDescriptorProvider interface { Fd() uintptr } func fileDescriptor(v any) (int, bool) { provider, ok := v.(fileDescriptorProvider) if !ok { return 0, false } return int(provider.Fd()), true } // daemonMode runs the chain serve command without user interaction, UI in verbose mode. Useful to be used as daemon. func daemonMode(cmd *cobra.Command) error { // always yes, no user interaction options := []cliui.Option{cliui.WithoutUserInteraction(true)} options = append(options, cliui.WithVerbosity(uilog.VerbosityVerbose), ) // output file logic outputFile, _ := cmd.Flags().GetString(flagOutputFile) var output *os.File var err error if outputFile != "" { output, err = os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } defer output.Close() options = append(options, cliui.WithStdout(output), cliui.WithStderr(output)) } else { options = append(options, cliui.WithStdout(os.Stdout), cliui.WithStderr(os.Stderr)) } session := cliui.New(options...) defer session.End() ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) defer stop() cmd.SetContext(ctx) return chainServe(cmd, session) } func chainServeCmd(cmd *cobra.Command, session *cliui.Session) tea.Cmd { return func() tea.Msg { if err := chainServe(cmd, session); err != nil && !errors.Is(err, context.Canceled) { return cliuimodel.ErrorMsg{Error: err} } return cliuimodel.QuitMsg{} } } func chainServe(cmd *cobra.Command, session *cliui.Session) error { chainOption := []chain.Option{ chain.WithOutputer(session), chain.CollectEvents(session.EventBus()), chain.CheckCosmosSDKVersion(), } if flagGetCheckDependencies(cmd) { chainOption = append(chainOption, chain.CheckDependencies()) } // check if custom config is defined config, _ := cmd.Flags().GetString(flagConfig) if config != "" { chainOption = append(chainOption, chain.ConfigFile(config)) } // create the chain c, err := chain.NewWithHomeFlags(cmd, chainOption...) if err != nil { return err } cacheStorage, err := newCache(cmd) if err != nil { return err } // serve the chain var serveOptions []chain.ServeOption forceUpdate, _ := cmd.Flags().GetBool(flagForceReset) if forceUpdate { serveOptions = append(serveOptions, chain.ServeForceReset()) } resetOnce, _ := cmd.Flags().GetBool(flagResetOnce) if resetOnce { serveOptions = append(serveOptions, chain.ServeResetOnce()) } quitOnFail, _ := cmd.Flags().GetBool(flagQuitOnFail) if quitOnFail { serveOptions = append(serveOptions, chain.QuitOnFail()) } generateClients, _ := cmd.Flags().GetBool(flagGenerateClients) if generateClients { serveOptions = append(serveOptions, chain.GenerateClients()) } buildTags, _ := cmd.Flags().GetStringSlice(flagBuildTags) if len(buildTags) > 0 { serveOptions = append(serveOptions, chain.BuildTags(buildTags...)) } if flagGetSkipProto(cmd) { serveOptions = append(serveOptions, chain.ServeSkipProto()) } if flagGetSkipBuild(cmd) { serveOptions = append(serveOptions, chain.ServeSkipBuild()) } if quitOnFail { serveOptions = append(serveOptions, chain.QuitOnFail()) } return c.Serve(cmd.Context(), cacheStorage, serveOptions...) }