Files

295 lines
10 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package networkcmd
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/luxfi/cli/pkg/binutils"
"github.com/luxfi/cli/pkg/snapshot"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/constants"
"github.com/luxfi/netrunner/local"
"github.com/luxfi/netrunner/server"
"github.com/spf13/cobra"
)
const networkTypeLocal = "local"
var (
stopNetworkType string
stopNetworkID uint32 // Custom network ID for non-standard networks
forceStop bool
stopCleanupLogs bool // Clean up old logs when stopping
// Network type flags (same as start command for consistency)
stopMainnet bool
stopTestnet bool
stopDevnet bool
)
func newStopCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "Stop the running network and save a snapshot",
Long: `The network stop command gracefully shuts down the running network and saves state.
SNAPSHOT BEHAVIOR:
By default, the network saves its state to a snapshot when stopping. This includes:
- Blockchain state (C-Chain, P-Chain, X-Chain, deployed chains)
- Validator state
- Database contents
The snapshot allows you to resume exactly where you left off with:
lux network start --<type> --snapshot-name <name>
OPTIONS:
--mainnet Stop mainnet network (network-id=1)
--testnet Stop testnet network (network-id=2)
--devnet Stop devnet network (network-id=3)
--network-id Stop network by ID (for custom networks)
--snapshot-name Name for the snapshot (default: default-snapshot)
--force Force stop without confirmation (use with caution)
SAFETY CHECKS:
If multiple networks are running, you MUST specify which one to stop:
lux network stop --devnet
lux network stop --testnet
Stopping mainnet or testnet requires explicit --mainnet/--testnet flag.
This prevents accidental disruption of production deployments.
EXAMPLES:
# Stop the running network (when only one is running)
lux network stop
# Stop specific network type (required when multiple running)
lux network stop --devnet
lux network stop --testnet
# Stop with named snapshot
lux network stop --devnet --snapshot-name my-snapshot
# Resume from snapshot later
lux network start --devnet --snapshot-name my-snapshot
NOTES:
- Snapshots preserve ALL network state including deployed chains
- Chain configurations (in ~/.lux/chains/) are NOT affected
- Use 'lux network clean' to wipe runtime data completely
- Only the specified network type is stopped (others remain running)
- Use 'lux dev stop' for the dev mode node (separate from network command)
SNAPSHOT vs CLEAN:
lux network stop - Saves state for resuming later
lux network clean - Deletes runtime data, preserves chain configs`,
RunE: StopNetwork,
Args: cobra.ExactArgs(0),
SilenceUsage: true,
}
cmd.Flags().StringVar(&snapshotName, "snapshot-name", constants.DefaultSnapshotName, "name of snapshot to use to save network state into")
cmd.Flags().BoolVar(&forceStop, "force", false, "force stop without confirmation (use with caution for mainnet/testnet)")
cmd.Flags().BoolVar(&stopCleanupLogs, "cleanup", false, "clean up old log files and stale run directories")
// Network type flags (same pattern as start command for consistency)
cmd.Flags().BoolVar(&stopMainnet, "mainnet", false, "stop mainnet network (network-id=1)")
cmd.Flags().BoolVar(&stopTestnet, "testnet", false, "stop testnet network (network-id=2)")
cmd.Flags().BoolVar(&stopDevnet, "devnet", false, "stop devnet network (network-id=3)")
cmd.Flags().Uint32Var(&stopNetworkID, "network-id", 0, "stop network by ID (for custom networks)")
return cmd
}
// StopNetwork stops the local network.
func StopNetwork(*cobra.Command, []string) error {
// Handle --mainnet, --testnet, --devnet flags (same pattern as start command)
if stopMainnet {
stopNetworkType = "mainnet"
} else if stopTestnet {
stopNetworkType = "testnet"
} else if stopDevnet {
stopNetworkType = "devnet"
} else if stopNetworkID > 0 {
// Map network ID to network type
switch stopNetworkID {
case 1:
stopNetworkType = "mainnet"
case 2:
stopNetworkType = "testnet"
case 3:
stopNetworkType = "devnet"
default:
stopNetworkType = fmt.Sprintf("custom-%d", stopNetworkID)
}
}
// Get all running networks
runningNetworks := app.GetAllRunningNetworks()
devRunning := isDevModeRunning()
// If network type not specified, apply safety checks
if stopNetworkType == "" {
// If multiple networks are running, require explicit --network-type
if len(runningNetworks) > 1 {
ux.Logger.PrintToUser("Multiple networks are running: %s", strings.Join(runningNetworks, ", "))
if devRunning {
ux.Logger.PrintToUser("Dev mode is also running (use 'lux dev stop' to stop it)")
}
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Please specify which network to stop:")
for _, net := range runningNetworks {
ux.Logger.PrintToUser(" lux network stop --%s", net)
}
return fmt.Errorf("ambiguous: multiple networks running. Use --%s to specify which one to stop", runningNetworks[0])
}
// If dev mode + one network, warn but allow stopping the network
if devRunning && len(runningNetworks) == 1 {
ux.Logger.PrintToUser("Note: Dev mode is also running. Use 'lux dev stop' to stop it separately.")
}
// Auto-detect the single running network
if len(runningNetworks) == 1 {
stopNetworkType = runningNetworks[0]
} else if len(runningNetworks) == 0 {
// No network running
if devRunning {
return fmt.Errorf("no network running. Dev mode is running - use 'lux dev stop' to stop it")
}
ux.Logger.PrintToUser("No network is currently running.")
return nil
}
}
// Normalize "local" to "custom"
if stopNetworkType == networkTypeLocal {
stopNetworkType = networkTypeCustom
}
// Safety check for mainnet/testnet: require explicit flag or force
if (stopNetworkType == "mainnet" || stopNetworkType == "testnet") && !forceStop {
// Check if this is a production-like network that needs protection
ux.Logger.PrintToUser("WARNING: You are about to stop %s network.", strings.ToUpper(stopNetworkType))
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("This could disrupt production services. Are you sure?")
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("To confirm, run:")
ux.Logger.PrintToUser(" lux network stop --%s --force", stopNetworkType)
return fmt.Errorf("stopping %s requires --force flag for safety", stopNetworkType)
}
// Check if the specified network is actually running
isRunning := false
for _, net := range runningNetworks {
if net == stopNetworkType {
isRunning = true
break
}
}
if !isRunning {
ux.Logger.PrintToUser("Network '%s' is not currently running.", stopNetworkType)
if len(runningNetworks) > 0 {
ux.Logger.PrintToUser("Running networks: %s", strings.Join(runningNetworks, ", "))
}
return nil
}
ux.Logger.PrintToUser("Stopping network: %s", stopNetworkType)
// Create hot snapshot via gRPC → admin.snapshot API while network is still running
// This is the primary method - uses native BadgerDB incremental backup
if err := saveNetworkForType(stopNetworkType); err != nil {
ux.Logger.PrintToUser("Warning: failed to save snapshot: %v", err)
}
if killErr := binutils.KillgRPCServerProcessForNetwork(app, stopNetworkType); killErr != nil {
app.Log.Warn("failed killing server process", "error", killErr)
ux.Logger.PrintToUser("Warning: failed to shutdown server gracefully: %v", killErr)
} else {
ux.Logger.PrintToUser("Server (%s) shutdown gracefully", stopNetworkType)
}
// Clear network-specific state when stopping
if clearErr := app.ClearNetworkStateForType(stopNetworkType); clearErr != nil {
app.Log.Warn("failed to clear network state", "error", clearErr)
}
// Cleanup old logs and stale runs if requested
if stopCleanupLogs {
ux.Logger.PrintToUser("Cleaning up old logs and stale run directories...")
sm := snapshot.NewSnapshotManager(app.GetBaseDir())
cfg := snapshot.DefaultCleanupConfig()
cfg.Verbose = true
result := sm.Cleanup(cfg)
if result.TotalBytesFreed() > 0 {
ux.Logger.PrintToUser("Freed %s (%d logs, %d backups, %d stale runs)",
snapshot.FormatBytes(result.TotalBytesFreed()),
result.LogsDeleted, result.BackupsDeleted, result.StaleRunsDeleted)
}
for _, err := range result.Errors {
ux.Logger.PrintToUser("Warning: %v", err)
}
}
return nil
}
// isDevModeRunning checks if a dev mode node is currently running
func isDevModeRunning() bool {
pidFile := filepath.Join(os.Getenv("HOME"), constants.BaseDirName, constants.DevDir, "luxd.pid")
pidData, err := os.ReadFile(pidFile) //nolint:gosec // G304: Reading PID file from app's directory
if err != nil {
return false
}
pid, err := strconv.Atoi(strings.TrimSpace(string(pidData)))
if err != nil {
return false
}
// Check if process exists using os.FindProcess
// Note: On Unix, FindProcess always succeeds, but we verify by checking /proc
// On Windows, we check if we can open the process
return isProcessRunning(pid)
}
func saveNetworkForType(networkType string) error {
cli, err := binutils.NewGRPCClient(binutils.WithAvoidRPCVersionCheck(true), binutils.WithNetworkType(networkType))
if err != nil {
return err
}
defer func() { _ = cli.Close() }()
ctx := binutils.GetAsyncContext()
_, err = cli.RemoveSnapshot(ctx, snapshotName)
if err != nil {
if server.IsServerError(err, server.ErrNotBootstrapped) {
ux.Logger.PrintToUser("Network already stopped.")
return nil
}
// it we try to stop a network with a new snapshot name, remove snapshot
// will fail, so we cover here that expected case
if !server.IsServerError(err, local.ErrSnapshotNotFound) {
return fmt.Errorf("failed stop network with a snapshot: %w", err)
}
}
_, err = cli.SaveSnapshot(ctx, snapshotName)
if err != nil {
return fmt.Errorf("failed to stop network with a snapshot: %w", err)
}
ux.Logger.PrintToUser("Network stopped successfully.")
return nil
}