// 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" "os" "os/signal" "path/filepath" "sync" "syscall" corelog "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" ) // newChainSyncCommand builds the `chain sync` command family. // // Example: // // chain sync // chain sync --daemon // chain sync --stop // // It keeps the foreground and daemon modes behind a predictable command path. func newChainSyncCommand(chainDataDir, seedPeerAddress *string, useTestnet *bool) *cobra.Command { var ( daemon bool stop bool ) cmd := &cobra.Command{ Use: "sync", Short: "Headless P2P chain sync", Long: "Sync the blockchain from P2P peers without the TUI explorer.", Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { if daemon && stop { return corelog.E("newChainSyncCommand", "flags --daemon and --stop cannot be combined", nil) } return validateChainOptions(*chainDataDir, *seedPeerAddress) }, RunE: func(cmd *cobra.Command, args []string) error { if stop { return stopChainSyncDaemon(*chainDataDir) } if daemon { return runChainSyncDaemon(*chainDataDir, *seedPeerAddress, *useTestnet) } return runChainSyncForeground(*chainDataDir, *seedPeerAddress, *useTestnet) }, } cmd.Flags().BoolVar(&daemon, "daemon", false, "run as background daemon") cmd.Flags().BoolVar(&stop, "stop", false, "stop a running sync daemon") return cmd } func runChainSyncForeground(chainDataDir, seedPeerAddress string, useTestnet bool) error { if err := ensureChainDataDirExists(chainDataDir); err != nil { return err } dbPath := filepath.Join(chainDataDir, "chain.db") chainStore, err := store.New(dbPath) if err != nil { return corelog.E("runChainSyncForeground", "open store", err) } defer chainStore.Close() blockchain := chain.New(chainStore) chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() corelog.Info("starting headless P2P sync", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet) runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed) corelog.Info("headless P2P sync stopped", "data_dir", chainDataDir) return nil } func runChainSyncDaemon(chainDataDir, seedPeerAddress string, useTestnet bool) error { if err := ensureChainDataDirExists(chainDataDir); err != nil { return err } pidFile := filepath.Join(chainDataDir, "sync.pid") daemon := process.NewDaemon(process.DaemonOptions{ PIDFile: pidFile, Registry: process.DefaultRegistry(), RegistryEntry: process.DaemonEntry{ Code: "dappco.re/go/core/blockchain", Daemon: "sync", }, }) if err := daemon.Start(); err != nil { return corelog.E("runChainSyncDaemon", "daemon start", err) } dbPath := filepath.Join(chainDataDir, "chain.db") chainStore, err := store.New(dbPath) if err != nil { _ = daemon.Stop() return corelog.E("runChainSyncDaemon", "open store", err) } defer chainStore.Close() blockchain := chain.New(chainStore) chainConfig, hardForks, resolvedSeed := chainConfigForSeed(useTestnet, seedPeerAddress) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() daemon.SetReady(true) corelog.Info("sync daemon started", "data_dir", chainDataDir, "seed", resolvedSeed, "testnet", useTestnet) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() runChainSyncLoop(ctx, blockchain, &chainConfig, hardForks, resolvedSeed) }() err = daemon.Run(ctx) wg.Wait() // Wait for the sync loop to finish before closing the store. return err } func stopChainSyncDaemon(chainDataDir string) error { pidFile := filepath.Join(chainDataDir, "sync.pid") pid, running := process.ReadPID(pidFile) if pid == 0 || !running { return corelog.E("stopChainSyncDaemon", "no running sync daemon found", nil) } processHandle, err := os.FindProcess(pid) if err != nil { return corelog.E("stopChainSyncDaemon", fmt.Sprintf("find process %d", pid), err) } if err := processHandle.Signal(syscall.SIGTERM); err != nil { return corelog.E("stopChainSyncDaemon", fmt.Sprintf("signal process %d", pid), err) } corelog.Info("sent SIGTERM to sync daemon", "pid", pid) return nil }