package tlp import ( "context" "fmt" "os" "path/filepath" "strings" "time" "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/system" "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types" "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils" ) type Manager struct { logger *utils.Logger } func NewManager(logger *utils.Logger) *Manager { return &Manager{ logger: logger.WithComponent("tlp"), } } func (m *Manager) GetStatus(ctx context.Context) (*types.TLPStatus, error) { status := &types.TLPStatus{ CurrentConfig: make(map[string]string), } if utils.CommandExists("tlp") { status.Installed = true if version, err := utils.RunCommand("tlp", "--version"); err == nil { parts := strings.Fields(version) if len(parts) >= 2 { status.Version = parts[1] } } if output, err := utils.RunCommand("systemctl", "is-active", "tlp"); err == nil { status.Active = strings.TrimSpace(output) == "active" } } configPaths := []string{ "/etc/tlp.conf", "/etc/default/tlp", } for _, path := range configPaths { if utils.FileExists(path) { status.ConfigPath = path status.ConfigExists = true if info, err := os.Stat(path); err == nil { modTime := info.ModTime() status.LastModified = &modTime } if config, err := m.readConfig(path); err == nil { status.CurrentConfig = config } break } } return status, nil } func (m *Manager) Install(ctx context.Context, sysInfo *system.Info) error { m.logger.Info("Installing TLP", "distro", sysInfo.Distribution, "package_manager", sysInfo.PackageManager) if utils.CommandExists("tlp") { return fmt.Errorf("TLP is already installed") } if !utils.IsRoot() { return fmt.Errorf("root privileges required for TLP installation") } var installCmd []string var updateCmd []string switch sysInfo.PackageManager { case "apt": updateCmd = []string{"apt", "update"} installCmd = []string{"apt", "install", "-y", "tlp", "tlp-rdw"} case "dnf": installCmd = []string{"dnf", "install", "-y", "tlp", "tlp-rdw"} case "yum": installCmd = []string{"yum", "install", "-y", "tlp", "tlp-rdw"} case "zypper": installCmd = []string{"zypper", "install", "-y", "tlp", "tlp-rdw"} case "pacman": updateCmd = []string{"pacman", "-Sy"} installCmd = []string{"pacman", "-S", "--noconfirm", "tlp", "tlp-rdw"} case "apk": installCmd = []string{"apk", "add", "tlp"} default: return fmt.Errorf("unsupported package manager: %s", sysInfo.PackageManager) } if len(updateCmd) > 0 { m.logger.Info("Updating package lists") if _, err := utils.RunCommand(updateCmd[0], updateCmd[1:]...); err != nil { m.logger.Warn("Failed to update package lists", "error", err) } } m.logger.Info("Installing TLP packages", "command", strings.Join(installCmd, " ")) if _, err := utils.RunCommand(installCmd[0], installCmd[1:]...); err != nil { return fmt.Errorf("failed to install TLP: %w", err) } m.logger.Info("Enabling TLP service") if _, err := utils.RunCommand("systemctl", "enable", "tlp"); err != nil { m.logger.Warn("Failed to enable TLP service", "error", err) } if _, err := utils.RunCommand("systemctl", "start", "tlp"); err != nil { m.logger.Warn("Failed to start TLP service", "error", err) } if utils.CommandExists("systemctl") { if _, err := utils.RunCommand("systemctl", "mask", "power-profiles-daemon"); err != nil { m.logger.Debug("power-profiles-daemon not found or already masked") } } m.logger.Info("TLP installation completed successfully") return nil } func (m *Manager) ApplyConfig(ctx context.Context, config *types.TLPConfiguration) error { m.logger.Info("Applying TLP configuration") if err := m.ValidateConfig(config); err != nil { return fmt.Errorf("configuration validation failed: %w", err) } configPath := "/etc/tlp.conf" if !utils.FileExists(configPath) { configPath = "/etc/default/tlp" } if utils.FileExists(configPath) { backupPath := fmt.Sprintf("%s.backup.%d", configPath, time.Now().Unix()) if err := m.backupConfig(configPath, backupPath); err != nil { m.logger.Warn("Failed to backup existing configuration", "error", err) } else { m.logger.Info("Backed up existing configuration", "backup", backupPath) } } if err := m.writeConfig(configPath, config); err != nil { return fmt.Errorf("failed to write configuration: %w", err) } m.logger.Info("Reloading TLP configuration") if _, err := utils.RunCommand("tlp", "start"); err != nil { m.logger.Warn("Failed to reload TLP configuration", "error", err) } m.logger.Info("TLP configuration applied successfully") return nil } func (m *Manager) ValidateConfig(config *types.TLPConfiguration) error { if config == nil { return fmt.Errorf("configuration is nil") } if len(config.Settings) == 0 { return fmt.Errorf("configuration has no settings") } for key, value := range config.Settings { if key == "" { return fmt.Errorf("empty setting key found") } switch key { case "TLP_ENABLE": if value != "1" && value != "0" { return fmt.Errorf("TLP_ENABLE must be 0 or 1, got: %s", value) } case "CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT": validGovernors := []string{"performance", "powersave", "ondemand", "conservative", "schedutil"} if !utils.Contains(validGovernors, value) { return fmt.Errorf("invalid CPU governor: %s", value) } } } return nil } func (m *Manager) readConfig(configPath string) (map[string]string, error) { config := make(map[string]string) lines, err := utils.ReadFileLines(configPath) if err != nil { return nil, err } for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if key, value, ok := utils.ParseKeyValue(line); ok { config[key] = value } } return config, nil } func (m *Manager) writeConfig(configPath string, config *types.TLPConfiguration) error { if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } file, err := os.OpenFile(configPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("failed to open config file: %w", err) } defer file.Close() fmt.Fprintf(file, "# TLP Configuration File\n") fmt.Fprintf(file, "# Generated by WiseTLP on %s\n", time.Now().Format("2006-01-02 15:04:05")) fmt.Fprintf(file, "# %s\n\n", config.Description) for key, value := range config.Settings { if rationale, exists := config.Rationale[key]; exists { fmt.Fprintf(file, "# %s\n", rationale) } fmt.Fprintf(file, "%s=%s\n\n", key, value) } if len(config.Warnings) > 0 { fmt.Fprintf(file, "# WARNINGS:\n") for _, warning := range config.Warnings { fmt.Fprintf(file, "# - %s\n", warning) } } return nil } func (m *Manager) backupConfig(configPath, backupPath string) error { input, err := os.ReadFile(configPath) if err != nil { return err } return os.WriteFile(backupPath, input, 0644) } type Configuration struct { *types.TLPConfiguration } func (c *Configuration) Present() error { fmt.Println("\n" + strings.Repeat("=", 60)) fmt.Println("GENERATED TLP CONFIGURATION") fmt.Println(strings.Repeat("=", 60)) fmt.Printf("\nDescription: %s\n", c.Description) if len(c.Warnings) > 0 { fmt.Println("\n⚠️ WARNINGS:") for _, warning := range c.Warnings { fmt.Printf(" - %s\n", warning) } } fmt.Println("\nConfiguration Settings:") fmt.Println(strings.Repeat("-", 40)) // Group settings by category for better presentation categories := map[string][]string{ "General": {"TLP_ENABLE", "TLP_WARN_LEVEL", "TLP_DEBUG"}, "CPU": {"CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT", "CPU_SCALING_MIN_FREQ_ON_AC", "CPU_SCALING_MAX_FREQ_ON_AC", "CPU_SCALING_MIN_FREQ_ON_BAT", "CPU_SCALING_MAX_FREQ_ON_BAT"}, "Platform": {"PLATFORM_PROFILE_ON_AC", "PLATFORM_PROFILE_ON_BAT"}, "Disk": {"DISK_APM_LEVEL_ON_AC", "DISK_APM_LEVEL_ON_BAT", "DISK_SPINDOWN_TIMEOUT_ON_AC", "DISK_SPINDOWN_TIMEOUT_ON_BAT"}, "Graphics": {"RADEON_DPM_STATE_ON_AC", "RADEON_DPM_STATE_ON_BAT"}, "Network": {"WIFI_PWR_ON_AC", "WIFI_PWR_ON_BAT"}, "USB": {"USB_AUTOSUSPEND", "USB_BLACKLIST"}, } for category, keys := range categories { hasSettings := false var categorySettings []string for _, key := range keys { if value, exists := c.Settings[key]; exists { if !hasSettings { categorySettings = append(categorySettings, fmt.Sprintf("\n%s:", category)) hasSettings = true } rationale := "" if r, exists := c.Rationale[key]; exists { rationale = fmt.Sprintf(" (%s)", r) } categorySettings = append(categorySettings, fmt.Sprintf(" %s = %s%s", key, value, rationale)) } } if hasSettings { for _, setting := range categorySettings { fmt.Println(setting) } } } // Show any remaining settings not in categories fmt.Println("\nOther Settings:") for key, value := range c.Settings { found := false for _, keys := range categories { if utils.Contains(keys, key) { found = true break } } if !found { rationale := "" if r, exists := c.Rationale[key]; exists { rationale = fmt.Sprintf(" (%s)", r) } fmt.Printf(" %s = %s%s\n", key, value, rationale) } } fmt.Println(strings.Repeat("=", 60)) // Get user approval fmt.Print("\nDo you want to apply this configuration? (y/N): ") var response string fmt.Scanln(&response) response = strings.ToLower(strings.TrimSpace(response)) if response != "y" && response != "yes" { return fmt.Errorf("configuration rejected by user") } return nil }