// Copyright (c) 2017-2026 Lethean (https://lt.hn) // // Licensed under the European Union Public Licence (EUPL) version 1.2. // SPDX-License-Identifier: EUPL-1.2 package blockchain import ( "context" "fmt" "log" "os" "os/signal" "path/filepath" "sync" "syscall" coreerr "dappco.re/go/core/log" "dappco.re/go/core/blockchain/chain" "dappco.re/go/core/process" store "dappco.re/go/core/store" "github.com/spf13/cobra" ) const chainSyncDisplayName = chainSyncCommandPath // newChainSyncCommand builds the `chain sync` command family. // // Example: // // core-chain chain sync // core-chain chain sync --daemon // core-chain chain sync --stop // // It keeps the foreground and background modes behind a predictable command path. func newChainSyncCommand(dataDir, seed *string, testnet *bool) *cobra.Command { var ( runAsDaemon bool stopDaemon bool ) cmd := &cobra.Command{ Use: chainSyncCommandName, Short: chainSyncCommandShort, Long: chainSyncCommandLong, Example: chainSyncExample, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if err := validateChainSyncModeSelection(runAsDaemon, stopDaemon); err != nil { return err } if stopDaemon { return stopChainSyncDaemon(*dataDir) } if runAsDaemon { return runChainSyncDaemon(*dataDir, *seed, *testnet) } return runChainSyncForeground(*dataDir, *seed, *testnet) }, } cmd.Flags().BoolVar(&runAsDaemon, "daemon", false, "keep syncing in the background") cmd.Flags().BoolVar(&stopDaemon, "stop", false, "stop the background sync for this data directory") return cmd } func runChainSyncForeground(dataDir, seed string, testnet bool) error { if err := validateChainRuntimeInputs(dataDir, seed); err != nil { return err } if err := ensureChainDataDirExists(dataDir); err != nil { return err } dbPath := filepath.Join(dataDir, "chain.db") chainStore, err := store.New(dbPath) if err != nil { return coreerr.E("runChainSyncForeground", "open store", err) } defer chainStore.Close() blockchain := chain.New(chainStore) chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() log.Print(formatChainSyncForegroundStart(dataDir, resolvedSeed)) runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed) log.Print(formatChainSyncForegroundStop()) return nil } func runChainSyncDaemon(dataDir, seed string, testnet bool) error { if err := validateChainRuntimeInputs(dataDir, seed); err != nil { return err } if err := ensureChainDataDirExists(dataDir); err != nil { return err } pidFile := filepath.Join(dataDir, "sync.pid") syncDaemon := process.NewDaemon(process.DaemonOptions{ PIDFile: pidFile, Registry: process.DefaultRegistry(), RegistryEntry: process.DaemonEntry{ Code: "dappco.re/go/core/blockchain", Daemon: "sync", }, }) if err := syncDaemon.Start(); err != nil { return coreerr.E("runChainSyncDaemon", "start background sync", err) } dbPath := filepath.Join(dataDir, "chain.db") chainStore, err := store.New(dbPath) if err != nil { _ = syncDaemon.Stop() return coreerr.E("runChainSyncDaemon", "open store", err) } defer chainStore.Close() blockchain := chain.New(chainStore) chainConfig, hardForks, resolvedSeed := chainConfigForSeed(testnet, seed) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() syncDaemon.SetReady(true) log.Print(formatChainSyncDaemonStarted(dataDir, resolvedSeed)) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed) }() err = syncDaemon.Run(ctx) wg.Wait() // Wait for the sync loop to finish before closing the store. return err } func stopChainSyncDaemon(dataDir string) error { if err := validateChainDataDir(dataDir); err != nil { return err } pidFile := filepath.Join(dataDir, "sync.pid") pid, running := process.ReadPID(pidFile) if pid == 0 || !running { return coreerr.E("stopChainSyncDaemon", "no running background sync process found", nil) } processHandle, err := os.FindProcess(pid) if err != nil { return coreerr.E("stopChainSyncDaemon", fmt.Sprintf("find process %d", pid), err) } if err := processHandle.Signal(syscall.SIGTERM); err != nil { return coreerr.E("stopChainSyncDaemon", fmt.Sprintf("signal process %d", pid), err) } log.Print(formatChainSyncDaemonStopping(pid)) return nil } func formatChainSyncForegroundStart(dataDir, seed string) string { return fmt.Sprintf("Starting chain sync in the foreground with data dir %q and seed %q. Press Ctrl+C to stop.", dataDir, seed) } func formatChainSyncForegroundStop() string { return "Chain sync stopped." } func formatChainSyncDaemonStarted(dataDir, seed string) string { return fmt.Sprintf("Background chain sync started with data dir %q and seed %q. Stop it with %s.", dataDir, seed, formatChainSyncStopCommand(dataDir)) } func formatChainSyncStopCommand(dataDir string) string { return fmt.Sprintf("%s --stop --data-dir %q", chainSyncCommandPath, dataDir) } func formatChainSyncDaemonStopping(pid int) string { return fmt.Sprintf("Stopping background chain sync (PID %d).", pid) }