359 lines
9.4 KiB
Go
359 lines
9.4 KiB
Go
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
|
||
}
|