This commit is contained in:
2025-09-16 14:27:34 +03:00
commit afeb139f5a
21 changed files with 4714 additions and 0 deletions

358
internal/tlp/manager.go Normal file
View File

@@ -0,0 +1,358 @@
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
}