From 3dabd93d2f9f5fe66cea0c8a8668091cb5343857 Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:44:47 +0800 Subject: [PATCH] Fix some problem. --- backend/internal/api/restore.go | 104 ++++++++++++++++++++++++ backend/internal/config/config.go | 18 ++++ backend/internal/config/store_sqlite.go | 15 ++-- backend/internal/kvm/kvm.go | 8 +- backend/internal/lxc/lxc.go | 8 +- backend/main.go | 19 +++++ 6 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 backend/internal/api/restore.go diff --git a/backend/internal/api/restore.go b/backend/internal/api/restore.go new file mode 100644 index 0000000..fe87ad2 --- /dev/null +++ b/backend/internal/api/restore.go @@ -0,0 +1,104 @@ +package api + +import ( + "fmt" + "time" + + "clicd/internal/config" + "clicd/internal/kvm" + "clicd/internal/lxc" +) + +// CaptureRuntimeRestoreState records which managed workloads are actually +// running before the CLICD service exits. On the next host boot, only those +// workloads are started again. +func CaptureRuntimeRestoreState() { + if config.AppConfig == nil { + return + } + lxcManager := lxc.NewManager() + kvmManager := kvm.NewManager() + changed := false + + for i := range config.AppConfig.Containers { + c := &config.AppConfig.Containers[i] + status, err := runtimeStatus(*c, lxcManager, kvmManager) + if err != nil { + fmt.Printf("Warning: failed to capture runtime state for %s: %v\n", c.Name, err) + continue + } + restore := status == "running" + if c.RestoreOnHostBoot != restore { + c.RestoreOnHostBoot = restore + changed = true + } + if status != "" && c.Status != status { + c.Status = status + changed = true + } + } + if changed { + if err := config.SaveConfig(); err != nil { + fmt.Printf("Warning: failed to save host boot restore state: %v\n", err) + } + } +} + +func StartHostBootRestore() { + go RestoreHostBootState() +} + +func RestoreHostBootState() { + if config.AppConfig == nil { + return + } + time.Sleep(2 * time.Second) + + lxcManager := lxc.NewManager() + kvmManager := kvm.NewManager() + containers := append([]config.Container(nil), config.AppConfig.Containers...) + + for _, c := range containers { + if !c.RestoreOnHostBoot { + continue + } + if c.PolicyBlocked { + fmt.Printf("Skipping host boot restore for %s: policy blocked\n", c.Name) + continue + } + if lxc.IsExpired(c) { + fmt.Printf("Skipping host boot restore for %s: expired at %s\n", c.Name, c.ExpiresAt) + continue + } + + status, err := runtimeStatus(c, lxcManager, kvmManager) + if err == nil && status == "running" { + config.UpdateContainerStatusAndRestore(c.ID, "running", true) + if !c.IsKVM() { + _ = lxcManager.ApplyPortMappings(c.ID) + } else { + _ = lxc.NewManager().ApplyPortMappings(c.ID) + } + continue + } + + fmt.Printf("Restoring workload after host boot: %s (ID=%d)\n", c.Name, c.ID) + if c.IsKVM() { + if err := kvmManager.StartContainer(c.ID); err != nil { + fmt.Printf("Warning: failed to restore KVM %s: %v\n", c.Name, err) + } + continue + } + if err := lxcManager.StartContainer(c.ID); err != nil { + fmt.Printf("Warning: failed to restore LXC %s: %v\n", c.Name, err) + } + } + lxc.EnsureAllRunningPortMappings() +} + +func runtimeStatus(c config.Container, lxcManager *lxc.Manager, kvmManager *kvm.Manager) (string, error) { + if c.IsKVM() { + return kvmManager.GetContainerStatus(c.VirshName()) + } + return lxcManager.GetContainerStatus(c.LxcName()) +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index b03ee34..3ac21e4 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -128,6 +128,7 @@ type Container struct { IOReadMBps int `json:"io_read_mbps"` IOWriteMBps int `json:"io_write_mbps"` Status string `json:"status"` + RestoreOnHostBoot bool `json:"restore_on_host_boot,omitempty"` IP string `json:"ip"` LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"` LANInterface string `json:"lan_interface,omitempty"` @@ -1218,6 +1219,23 @@ func UpdateContainerStatus(id int, status string) { } } +func UpdateContainerStatusAndRestore(id int, status string, restoreOnHostBoot bool) { + c := FindContainer(id) + if c != nil { + c.Status = status + c.RestoreOnHostBoot = restoreOnHostBoot + SaveConfig() + } +} + +func SetContainerRestoreOnHostBoot(id int, restore bool) { + c := FindContainer(id) + if c != nil { + c.RestoreOnHostBoot = restore + SaveConfig() + } +} + func SetContainerPolicyBlock(id int, blocked bool, reason string) { c := FindContainer(id) if c == nil { diff --git a/backend/internal/config/store_sqlite.go b/backend/internal/config/store_sqlite.go index 5620705..5d8c2d4 100644 --- a/backend/internal/config/store_sqlite.go +++ b/backend/internal/config/store_sqlite.go @@ -209,6 +209,7 @@ func ensureSchema() error { io_read_mbps INTEGER NOT NULL DEFAULT 0, io_write_mbps INTEGER NOT NULL DEFAULT 0, status TEXT, + restore_on_host_boot INTEGER NOT NULL DEFAULT 0, ip TEXT, lan_ipv4_mode TEXT, lan_interface TEXT, @@ -459,6 +460,7 @@ func ensureSchemaMigrations() error { {"containers", "firewall_rules", "TEXT"}, {"containers", "allowed_image_ids", "TEXT"}, {"containers", "image_limit_configured", "INTEGER NOT NULL DEFAULT 0"}, + {"containers", "restore_on_host_boot", "INTEGER NOT NULL DEFAULT 0"}, {"containers", "lan_ipv4_mode", "TEXT"}, {"containers", "lan_interface", "TEXT"}, {"containers", "lan_ipv4_address", "TEXT NOT NULL DEFAULT ''"}, @@ -739,20 +741,20 @@ func saveContainers(tx *sql.Tx) error { monthly_traffic_gb, traffic_mode, traffic_in_gb, traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date, io_speed_mbps, io_read_mbps, io_write_mbps, - status, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway, + status, restore_on_host_boot, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway, ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password, ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at, snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time, snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by, policy_blocked, policy_blocked_reason, policy_blocked_at, firewall_enabled, firewall_default_action, firewall_rules, allowed_image_ids, image_limit_configured - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template, c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.NetworkDownMbps, c.NetworkUpMbps, c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB, c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate, c.IOSpeedMBps, c.IOReadMBps, c.IOWriteMBps, - c.Status, c.IP, c.LANIPv4Mode, c.LANInterface, c.LANIPv4Address, c.LANIPv4PrefixLen, c.LANIPv4Gateway, + c.Status, boolInt(c.RestoreOnHostBoot), c.IP, c.LANIPv4Mode, c.LANInterface, c.LANIPv4Address, c.LANIPv4PrefixLen, c.LANIPv4Gateway, c.IPv6, c.IPv6PrefixLen, c.IPv6Interface, c.VNCPort, c.SSHPort, c.SSHPassword, c.SSHHostKey, c.PortMappingLimit, c.SnapshotLimit, c.CreatedAt, c.ExpiresAt, boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime, @@ -960,7 +962,7 @@ func loadContainers() ([]Container, error) { monthly_traffic_gb, traffic_mode, traffic_in_gb, traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date, io_speed_mbps, io_read_mbps, io_write_mbps, - status, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway, + status, restore_on_host_boot, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway, ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password, ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at, snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time, @@ -976,7 +978,7 @@ func loadContainers() ([]Container, error) { result := []Container{} for rows.Next() { var c Container - var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured int + var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured, restoreOnHostBoot int var firewallDefaultAction string var firewallRulesJSON, allowedImageIDs sql.NullString var lanIPv4Mode, lanInterface sql.NullString @@ -988,7 +990,7 @@ func loadContainers() ([]Container, error) { &c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB, &c.TrafficOutGB, &c.TrafficUsedRX, &c.TrafficUsedTX, &c.TrafficResetDate, &c.IOSpeedMBps, &c.IOReadMBps, &c.IOWriteMBps, - &c.Status, &c.IP, &lanIPv4Mode, &lanInterface, &lanIPv4Address, &lanIPv4PrefixLen, &lanIPv4Gateway, + &c.Status, &restoreOnHostBoot, &c.IP, &lanIPv4Mode, &lanInterface, &lanIPv4Address, &lanIPv4PrefixLen, &lanIPv4Gateway, &c.IPv6, &c.IPv6PrefixLen, &c.IPv6Interface, &c.VNCPort, &c.SSHPort, &c.SSHPassword, &c.SSHHostKey, &c.PortMappingLimit, &c.SnapshotLimit, &c.CreatedAt, &c.ExpiresAt, &scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime, @@ -1006,6 +1008,7 @@ func loadContainers() ([]Container, error) { } c.LANIPv4Gateway = lanIPv4Gateway.String c.SnapshotScheduleEnabled = scheduleEnabled != 0 + c.RestoreOnHostBoot = restoreOnHostBoot != 0 c.PolicyBlocked = policyBlocked != 0 c.FirewallEnabled = firewallEnabled != 0 c.FirewallDefaultAction = normalizeFirewallDefaultAction(firewallDefaultAction) diff --git a/backend/internal/kvm/kvm.go b/backend/internal/kvm/kvm.go index 61f686c..4c52542 100644 --- a/backend/internal/kvm/kvm.go +++ b/backend/internal/kvm/kvm.go @@ -649,7 +649,7 @@ func (m *Manager) StartContainer(id int) error { if !isWindows && c.IP != "" { m.waitForCloudInitReady(name, c.IP, c.SSHPassword) } - config.UpdateContainerStatus(id, "running") + config.UpdateContainerStatusAndRestore(id, "running", true) if c.IPv6 != "" || len(c.IPv6Addresses) > 0 { if err := m.applyIPv6Runtime(c); err != nil { return err @@ -728,13 +728,13 @@ func (m *Manager) StopContainer(id int) error { name := c.VirshName() status, _ := m.GetContainerStatus(name) if status != "running" { - config.UpdateContainerStatus(id, "stopped") + config.UpdateContainerStatusAndRestore(id, "stopped", false) return nil } exec.Command("virsh", "shutdown", name).Run() for i := 0; i < 20; i++ { if status, _ := m.GetContainerStatus(name); status != "running" { - config.UpdateContainerStatus(id, "stopped") + config.UpdateContainerStatusAndRestore(id, "stopped", false) return nil } time.Sleep(1 * time.Second) @@ -743,7 +743,7 @@ func (m *Manager) StopContainer(id int) error { if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("virsh destroy failed: %v, output: %s", err, string(output)) } - config.UpdateContainerStatus(id, "stopped") + config.UpdateContainerStatusAndRestore(id, "stopped", false) return nil } diff --git a/backend/internal/lxc/lxc.go b/backend/internal/lxc/lxc.go index 9c4a0a7..b19ba7e 100644 --- a/backend/internal/lxc/lxc.go +++ b/backend/internal/lxc/lxc.go @@ -1730,7 +1730,7 @@ func (m *Manager) StartContainer(id int) error { return err } - config.UpdateContainerStatus(id, "running") + config.UpdateContainerStatusAndRestore(id, "running", true) var ip string for retry := 0; retry < 10; retry++ { @@ -1951,7 +1951,7 @@ func (m *Manager) StopContainer(id int) error { status, _ := m.GetContainerStatus(lxcName) if status != "running" { - config.UpdateContainerStatus(id, "stopped") + config.UpdateContainerStatusAndRestore(id, "stopped", false) m.CleanPortMappings(id) CleanFirewallRules(id) m.cleanupBandwidthLimit(lxcName) @@ -1966,13 +1966,13 @@ func (m *Manager) StopContainer(id int) error { output, err := cmd.CombinedOutput() if err != nil { if strings.Contains(string(output), "not running") { - config.UpdateContainerStatus(id, "stopped") + config.UpdateContainerStatusAndRestore(id, "stopped", false) return nil } return fmt.Errorf("failed to stop container: %v, output: %s", err, string(output)) } - config.UpdateContainerStatus(id, "stopped") + config.UpdateContainerStatusAndRestore(id, "stopped", false) fmt.Printf("Container %d (%s) stopped\n", id, c.Name) return nil } diff --git a/backend/main.go b/backend/main.go index a2f7e44..66cd624 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,7 +4,10 @@ import ( "fmt" "os" "os/exec" + "os/signal" "strings" + "sync" + "syscall" "clicd/internal/api" "clicd/internal/cli" @@ -16,6 +19,8 @@ import ( "golang.org/x/term" ) +var shutdownCaptureOnce sync.Once + func main() { isTerminal := term.IsTerminal(int(os.Stdin.Fd())) @@ -44,6 +49,8 @@ func main() { _ = cfg if isServerMode || (!isTerminal && !isCliMode) { + installShutdownStateCapture() + // Restore persisted state api.RestoreTasks() api.RestoreLoginLogs() @@ -75,6 +82,7 @@ func main() { // Clean up stale container configs (LXC dir was deleted but config remains) config.CleanStaleContainers() + api.StartHostBootRestore() lxc.EnsureAllRunningPortMappings() // Pre-warm SSH for containers already running after host boot or service restart. @@ -97,6 +105,17 @@ func main() { } } +func installShutdownStateCapture() { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-signals + fmt.Fprintf(os.Stderr, "Received %s, capturing workload restore state...\n", sig) + shutdownCaptureOnce.Do(api.CaptureRuntimeRestoreState) + os.Exit(0) + }() +} + func isWebPanelSystemdRunning() bool { cmd := exec.Command("systemctl", "is-active", "clicd") output, err := cmd.Output()