Files
WiseTLP/internal/tlp/manager.go
2025-09-16 14:27:34 +03:00

359 lines
9.4 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}