yoohoo
This commit is contained in:
350
README.md
Normal file
350
README.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# WiseTLP
|
||||||
|
|
||||||
|
**AI-powered TLP configuration automation tool for Linux power management optimization**
|
||||||
|
|
||||||
|
WiseTLP is a professional-grade command-line application that leverages artificial intelligence to automatically generate optimized TLP (Linux Advanced Power Management) configurations tailored to your specific system hardware and usage patterns.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
|
||||||
|
- **Comprehensive System Detection**: Automatically detects Linux distribution, hardware specifications, power management capabilities, and existing configurations
|
||||||
|
- **AI-Powered Configuration Generation**: Uses advanced AI models to generate optimized TLP configurations based on your system and preferences
|
||||||
|
- **Multi-Provider AI Support**: Compatible with multiple AI services including Groq, OpenRouter, Gemini, and custom OpenAI-compatible endpoints
|
||||||
|
- **Interactive User Interface**: Guided configuration process with clear explanations and user-friendly prompts
|
||||||
|
- **Secure API Key Management**: Encrypted storage of API keys with master password protection
|
||||||
|
- **Automated TLP Installation**: Handles TLP installation across major Linux distributions with proper privilege escalation
|
||||||
|
|
||||||
|
### System Compatibility
|
||||||
|
|
||||||
|
WiseTLP supports major Linux distributions including:
|
||||||
|
|
||||||
|
- **Debian-based**: Ubuntu, Debian, Linux Mint, Elementary OS, Pop!_OS
|
||||||
|
- **Red Hat-based**: Fedora, RHEL, CentOS, Rocky Linux, AlmaLinux
|
||||||
|
- **Arch-based**: Arch Linux, Manjaro, EndeavourOS, Garuda Linux
|
||||||
|
- **SUSE-based**: openSUSE Leap, openSUSE Tumbleweed
|
||||||
|
- **Other**: Alpine Linux, Gentoo
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
|
||||||
|
- **Encrypted API Key Storage**: API keys are encrypted using AES-GCM with master password protection
|
||||||
|
- **Secure Privilege Escalation**: Minimal privilege escalation with user confirmation and secure sudo handling
|
||||||
|
- **Input Validation**: Comprehensive input sanitization and validation to prevent security vulnerabilities
|
||||||
|
- **Configuration Backup**: Automatic backup of existing TLP configurations before applying changes
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Linux operating system (kernel 2.6.32 or later)
|
||||||
|
- Go 1.22 or later (for building from source)
|
||||||
|
- sudo privileges (for TLP installation and configuration)
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp.git
|
||||||
|
cd autotlp
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
go build -o autotlp ./cmd/autotlp
|
||||||
|
|
||||||
|
# Install to system path (optional)
|
||||||
|
sudo mv autotlp /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run WiseTLP with interactive setup
|
||||||
|
./autotlp
|
||||||
|
|
||||||
|
# Or if installed system-wide
|
||||||
|
autotlp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
WiseTLP provides an interactive command-line interface that guides you through the configuration process:
|
||||||
|
|
||||||
|
1. **System Detection**: WiseTLP automatically detects your system specifications
|
||||||
|
2. **TLP Installation**: If TLP is not installed, WiseTLP can install it for you
|
||||||
|
3. **AI Service Configuration**: Choose and configure your preferred AI service
|
||||||
|
4. **User Preferences**: Specify your power management preferences and use case
|
||||||
|
5. **Configuration Generation**: AI generates an optimized TLP configuration
|
||||||
|
6. **Review and Apply**: Review the generated configuration and apply it to your system
|
||||||
|
|
||||||
|
### AI Service Configuration
|
||||||
|
|
||||||
|
WiseTLP supports multiple AI providers:
|
||||||
|
|
||||||
|
#### Groq (Recommended for speed)
|
||||||
|
```
|
||||||
|
Provider: Groq
|
||||||
|
Endpoint: https://api.groq.com/openai/v1/chat/completions
|
||||||
|
Model: openai/gpt-oss-20b
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenRouter (Multiple model options)
|
||||||
|
```
|
||||||
|
Provider: OpenRouter
|
||||||
|
Endpoint: https://openrouter.ai/api/v1/chat/completions
|
||||||
|
Model: meta-llama/llama-3.1-8b-instruct:free (or others)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Google Gemini
|
||||||
|
```
|
||||||
|
Provider: Gemini
|
||||||
|
Endpoint: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent
|
||||||
|
Model: gemini-pro
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom OpenAI-compatible
|
||||||
|
```
|
||||||
|
Provider: Custom
|
||||||
|
Endpoint: [Your custom endpoint]
|
||||||
|
Model: [Your model name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
#### Power Profiles
|
||||||
|
|
||||||
|
- **Balanced**: Optimal balance between performance and power efficiency
|
||||||
|
- **Performance**: Maximum performance with higher power consumption
|
||||||
|
- **Power Saving**: Maximum battery life with reduced performance
|
||||||
|
- **Custom**: Specify custom requirements and settings
|
||||||
|
|
||||||
|
#### Use Cases
|
||||||
|
|
||||||
|
- **General**: Web browsing, office work, media consumption
|
||||||
|
- **Development**: Programming, compiling, development tools
|
||||||
|
- **Gaming**: Gaming and graphics-intensive applications
|
||||||
|
- **Server**: Server workloads, always-on services
|
||||||
|
- **Multimedia**: Video editing, rendering, content creation
|
||||||
|
- **Office**: Primarily office applications and productivity
|
||||||
|
|
||||||
|
#### Special Requirements
|
||||||
|
|
||||||
|
- Minimize fan noise
|
||||||
|
- Prevent thermal throttling
|
||||||
|
- Optimize for external displays
|
||||||
|
- Gaming performance priority
|
||||||
|
- Maximum WiFi performance
|
||||||
|
- Minimize disk wear
|
||||||
|
- Fast system wake/sleep
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Example 1: Gaming Laptop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Power Profile: Performance
|
||||||
|
Use Case: Gaming
|
||||||
|
Battery Priority: Balanced
|
||||||
|
Performance Mode: Maximum
|
||||||
|
Special Requirements: Gaming performance priority, Prevent thermal throttling
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated settings include:
|
||||||
|
- CPU governor: performance on AC, balanced on battery
|
||||||
|
- Platform profile: performance
|
||||||
|
- Aggressive GPU power management
|
||||||
|
- Optimized disk settings for game loading
|
||||||
|
|
||||||
|
### Example 2: Development Workstation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Power Profile: Balanced
|
||||||
|
Use Case: Development
|
||||||
|
Battery Priority: Runtime
|
||||||
|
Performance Mode: Adaptive
|
||||||
|
Special Requirements: Fast system wake/sleep, Minimize fan noise
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated settings include:
|
||||||
|
- CPU governor: ondemand on AC, powersave on battery
|
||||||
|
- Balanced disk APM levels
|
||||||
|
- Conservative thermal management
|
||||||
|
- Optimized compilation performance
|
||||||
|
|
||||||
|
### Example 3: Ultrabook for Office Work
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Power Profile: Power Saving
|
||||||
|
Use Case: Office
|
||||||
|
Battery Priority: Longevity
|
||||||
|
Performance Mode: Efficient
|
||||||
|
Special Requirements: Minimize fan noise, Maximum WiFi performance
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated settings include:
|
||||||
|
- CPU governor: powersave
|
||||||
|
- Aggressive power management
|
||||||
|
- Conservative disk settings
|
||||||
|
- Optimized for battery longevity
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
WiseTLP follows a modular architecture with clear separation of concerns:
|
||||||
|
|
||||||
|
```
|
||||||
|
autotlp/
|
||||||
|
├── cmd/autotlp/ # Main application entry point
|
||||||
|
├── internal/ # Internal application modules
|
||||||
|
│ ├── ai/ # AI client implementation
|
||||||
|
│ ├── config/ # Configuration management
|
||||||
|
│ ├── security/ # Security and privilege management
|
||||||
|
│ ├── system/ # System detection and information gathering
|
||||||
|
│ └── tlp/ # TLP installation and configuration
|
||||||
|
├── pkg/ # Public packages
|
||||||
|
│ ├── types/ # Type definitions
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
├── test/ # Integration tests
|
||||||
|
├── docs/ # Documentation
|
||||||
|
└── examples/ # Example configurations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
- **System Detector**: Identifies Linux distribution, hardware, and power management capabilities
|
||||||
|
- **AI Client**: Handles communication with various AI service providers
|
||||||
|
- **TLP Manager**: Manages TLP installation, configuration, and validation
|
||||||
|
- **Security Manager**: Handles privilege escalation and secure key storage
|
||||||
|
- **Configuration Engine**: Processes user preferences and generates TLP settings
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
WiseTLP implements several security measures:
|
||||||
|
|
||||||
|
### API Key Protection
|
||||||
|
- API keys are encrypted using AES-GCM with a user-provided master password
|
||||||
|
- Keys are stored in `~/.config/autotlp/` with restrictive permissions (0600)
|
||||||
|
- Master password is hashed using SHA-256 for verification
|
||||||
|
|
||||||
|
### Privilege Escalation
|
||||||
|
- Minimal privilege escalation using sudo only when necessary
|
||||||
|
- Clear user prompts explaining why elevated privileges are required
|
||||||
|
- Secure handling of elevated processes with proper exit code propagation
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
- Comprehensive input sanitization to prevent injection attacks
|
||||||
|
- Path validation to prevent directory traversal vulnerabilities
|
||||||
|
- Configuration validation to ensure system stability
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
WiseTLP includes comprehensive test coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Run specific package tests
|
||||||
|
go test ./pkg/utils
|
||||||
|
go test ./internal/system
|
||||||
|
go test ./internal/config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
- **Unit Tests**: Test individual functions and components
|
||||||
|
- **Integration Tests**: Test component interactions
|
||||||
|
- **System Tests**: Test on various Linux distributions (manual)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions to WiseTLP! Please follow these guidelines:
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch: `git checkout -b feature-name`
|
||||||
|
3. Make your changes with proper tests
|
||||||
|
4. Ensure all tests pass: `go test ./...`
|
||||||
|
5. Follow Go coding standards and run `gofmt`
|
||||||
|
6. Submit a pull request with a clear description
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
|
||||||
|
- Follow Go best practices and idiomatic patterns
|
||||||
|
- Include comprehensive error handling
|
||||||
|
- Add tests for new functionality
|
||||||
|
- Update documentation for API changes
|
||||||
|
- Use structured logging with appropriate log levels
|
||||||
|
|
||||||
|
### Reporting Issues
|
||||||
|
|
||||||
|
Please report bugs and feature requests through GitHub Issues:
|
||||||
|
|
||||||
|
1. Check existing issues first
|
||||||
|
2. Provide detailed system information
|
||||||
|
3. Include steps to reproduce bugs
|
||||||
|
4. Attach relevant log output or configuration files
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### TLP Installation Fails
|
||||||
|
```
|
||||||
|
Error: failed to install TLP: permission denied
|
||||||
|
```
|
||||||
|
**Solution**: Ensure you have sudo privileges and run with appropriate permissions.
|
||||||
|
|
||||||
|
#### AI API Connection Issues
|
||||||
|
```
|
||||||
|
Error: failed to validate AI connection: connection timeout
|
||||||
|
```
|
||||||
|
**Solution**: Check your internet connection, API key validity, and endpoint URL.
|
||||||
|
|
||||||
|
#### System Detection Problems
|
||||||
|
```
|
||||||
|
Error: failed to detect system: unsupported distribution
|
||||||
|
```
|
||||||
|
**Solution**: WiseTLP supports major distributions. For unsupported distributions, please file an issue.
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging for troubleshooting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TLP_DEBUG=1
|
||||||
|
./autotlp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
Application logs are written to:
|
||||||
|
- stdout/stderr for interactive sessions
|
||||||
|
- System journal when run as a service
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
WiseTLP is designed for efficiency:
|
||||||
|
|
||||||
|
- **Startup Time**: < 1 second for system detection
|
||||||
|
- **AI Response Time**: 5-30 seconds depending on provider and model
|
||||||
|
- **Configuration Application**: < 5 seconds
|
||||||
|
- **Memory Usage**: < 50MB during operation
|
||||||
|
- **Binary Size**: < 10MB compiled
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Version History
|
||||||
|
|
||||||
|
- **v1.0.0**: Initial release with core functionality
|
||||||
|
- AI-powered configuration generation
|
||||||
|
- Multi-provider AI support
|
||||||
|
- Comprehensive system detection
|
||||||
|
- Secure API key management
|
||||||
|
- Automated TLP installation
|
||||||
|
|
||||||
|
|
||||||
|
**WiseTLP** - Intelligent power management for the modern Linux desktop and server.
|
152
cmd/autotlp/main.go
Normal file
152
cmd/autotlp/main.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/ai"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/config"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/system"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/tlp"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
appName = "WiseTLP"
|
||||||
|
appVersion = "1.0.0"
|
||||||
|
appDesc = "AI-powered TLP configuration automation tool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
fmt.Println("\nReceived interrupt signal. Shutting down gracefully...")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger := utils.NewLogger()
|
||||||
|
app := &Application{
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
config: config.New(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(); err != nil {
|
||||||
|
logger.Error("Application failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger *utils.Logger
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) Run() error {
|
||||||
|
a.logger.Info("Starting WiseTLP", "version", appVersion)
|
||||||
|
a.displayWelcome()
|
||||||
|
|
||||||
|
systemInfo, err := system.DetectSystem(a.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to detect system: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("System detected",
|
||||||
|
"distro", systemInfo.Distribution,
|
||||||
|
"version", systemInfo.Version,
|
||||||
|
"arch", systemInfo.Architecture)
|
||||||
|
|
||||||
|
tlpManager := tlp.NewManager(a.logger)
|
||||||
|
tlpStatus, err := tlpManager.GetStatus(a.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check TLP status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tlpStatus.Installed {
|
||||||
|
if err := a.handleTLPInstallation(tlpManager, systemInfo); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle TLP installation: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sysInfo, err := system.GatherSystemInfo(a.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to gather system information: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences, err := a.getUserPreferences()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user preferences: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aiClient, err := a.configureAIClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to configure AI client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlpConfig, err := aiClient.GenerateConfig(a.ctx, sysInfo, preferences)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate TLP configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.presentConfiguration(tlpConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed to present configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tlpManager.ApplyConfig(a.ctx, tlpConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("TLP configuration applied successfully")
|
||||||
|
fmt.Println("\n✓ TLP configuration has been successfully applied!")
|
||||||
|
fmt.Println("Your system is now optimized for your specified use case.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) displayWelcome() {
|
||||||
|
fmt.Printf(`
|
||||||
|
%s v%s
|
||||||
|
%s
|
||||||
|
|
||||||
|
This tool will help you configure TLP (Linux Advanced Power Management)
|
||||||
|
using AI assistance to optimize your system's power management settings.
|
||||||
|
|
||||||
|
`, appName, appVersion, appDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) handleTLPInstallation(manager *tlp.Manager, sysInfo *system.Info) error {
|
||||||
|
fmt.Println("TLP is not installed on your system.")
|
||||||
|
fmt.Print("Would you like to install it now? (y/N): ")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
|
||||||
|
if response == "y" || response == "Y" || response == "yes" {
|
||||||
|
return manager.Install(a.ctx, sysInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("TLP installation is required to proceed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) getUserPreferences() (*types.UserPreferences, error) {
|
||||||
|
return config.GatherUserPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) configureAIClient() (*ai.Client, error) {
|
||||||
|
return ai.ConfigureClient(a.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) presentConfiguration(config *types.TLPConfiguration) error {
|
||||||
|
tlpConfig := &tlp.Configuration{TLPConfiguration: config}
|
||||||
|
return tlpConfig.Present()
|
||||||
|
}
|
376
docs/API.md
Normal file
376
docs/API.md
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
# WiseTLP API Documentation
|
||||||
|
|
||||||
|
This document provides detailed information about WiseTLP's internal APIs and interfaces.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [System Detection API](#system-detection-api)
|
||||||
|
- [AI Client API](#ai-client-api)
|
||||||
|
- [TLP Management API](#tlp-management-api)
|
||||||
|
- [Security API](#security-api)
|
||||||
|
- [Configuration API](#configuration-api)
|
||||||
|
- [Types and Structures](#types-and-structures)
|
||||||
|
|
||||||
|
## System Detection API
|
||||||
|
|
||||||
|
The system detection module provides comprehensive Linux system information gathering.
|
||||||
|
|
||||||
|
### `system.DetectSystem(ctx context.Context) (*Info, error)`
|
||||||
|
|
||||||
|
Detects basic system information including distribution, version, and package manager.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ctx`: Context for cancellation and timeout control
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*Info`: Basic system information structure
|
||||||
|
- `error`: Error if detection fails
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```go
|
||||||
|
ctx := context.Background()
|
||||||
|
info, err := system.DetectSystem(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Distribution: %s %s\n", info.Distribution, info.Version)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `system.GatherSystemInfo(ctx context.Context) (*types.SystemInfo, error)`
|
||||||
|
|
||||||
|
Collects comprehensive system information including hardware, power management, and kernel details.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ctx`: Context for cancellation and timeout control
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*types.SystemInfo`: Complete system information structure
|
||||||
|
- `error`: Error if gathering fails
|
||||||
|
|
||||||
|
**Gathered Information:**
|
||||||
|
- CPU details (model, cores, frequency, governor)
|
||||||
|
- Memory information (total, available, swap)
|
||||||
|
- Battery information (if present)
|
||||||
|
- Power supply status
|
||||||
|
- Kernel information and parameters
|
||||||
|
- Hardware details (chassis, manufacturer, storage)
|
||||||
|
- Distribution information
|
||||||
|
|
||||||
|
## AI Client API
|
||||||
|
|
||||||
|
The AI client module handles communication with various AI service providers.
|
||||||
|
|
||||||
|
### `ai.NewClient(config *types.AIConfig, logger *utils.Logger) *Client`
|
||||||
|
|
||||||
|
Creates a new AI client with the specified configuration.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `config`: AI service configuration
|
||||||
|
- `logger`: Logger instance for structured logging
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*Client`: Configured AI client instance
|
||||||
|
|
||||||
|
### `ai.ConfigureClient(logger *utils.Logger) (*Client, error)`
|
||||||
|
|
||||||
|
Interactively configures an AI client by prompting the user for provider selection and credentials.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `logger`: Logger instance
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*Client`: Configured AI client
|
||||||
|
- `error`: Configuration error
|
||||||
|
|
||||||
|
### `(*Client).GenerateConfig(ctx context.Context, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error)`
|
||||||
|
|
||||||
|
Generates an optimized TLP configuration using AI based on system information and user preferences.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ctx`: Context for request timeout
|
||||||
|
- `sysInfo`: Complete system information
|
||||||
|
- `preferences`: User power management preferences
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*types.TLPConfiguration`: Generated TLP configuration
|
||||||
|
- `error`: Generation error
|
||||||
|
|
||||||
|
## TLP Management API
|
||||||
|
|
||||||
|
The TLP management module handles TLP installation, configuration, and validation.
|
||||||
|
|
||||||
|
### `tlp.NewManager(logger *utils.Logger) *Manager`
|
||||||
|
|
||||||
|
Creates a new TLP manager instance.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `logger`: Logger instance
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*Manager`: TLP manager instance
|
||||||
|
|
||||||
|
### `(*Manager).GetStatus(ctx context.Context) (*types.TLPStatus, error)`
|
||||||
|
|
||||||
|
Retrieves the current TLP installation and configuration status.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*types.TLPStatus`: Current TLP status
|
||||||
|
- `error`: Status retrieval error
|
||||||
|
|
||||||
|
### `(*Manager).Install(ctx context.Context, sysInfo *system.Info) error`
|
||||||
|
|
||||||
|
Installs TLP using the appropriate package manager for the detected distribution.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ctx`: Context for operation timeout
|
||||||
|
- `sysInfo`: System information for package manager selection
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `error`: Installation error
|
||||||
|
|
||||||
|
**Supported Package Managers:**
|
||||||
|
- apt (Ubuntu, Debian)
|
||||||
|
- dnf (Fedora, RHEL, CentOS)
|
||||||
|
- pacman (Arch, Manjaro)
|
||||||
|
- zypper (openSUSE)
|
||||||
|
- yum (Legacy RHEL/CentOS)
|
||||||
|
- apk (Alpine)
|
||||||
|
|
||||||
|
### `(*Manager).ApplyConfig(ctx context.Context, config *types.TLPConfiguration) error`
|
||||||
|
|
||||||
|
Applies a TLP configuration to the system.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `ctx`: Context for operation timeout
|
||||||
|
- `config`: TLP configuration to apply
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `error`: Application error
|
||||||
|
|
||||||
|
**Operations:**
|
||||||
|
1. Validates configuration
|
||||||
|
2. Backs up existing configuration
|
||||||
|
3. Writes new configuration
|
||||||
|
4. Reloads TLP service
|
||||||
|
|
||||||
|
### `(*Manager).ValidateConfig(config *types.TLPConfiguration) error`
|
||||||
|
|
||||||
|
Validates a TLP configuration for correctness and safety.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `config`: Configuration to validate
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `error`: Validation error
|
||||||
|
|
||||||
|
## Security API
|
||||||
|
|
||||||
|
The security module handles API key storage and privilege escalation.
|
||||||
|
|
||||||
|
### `security.NewKeyStore() (*KeyStore, error)`
|
||||||
|
|
||||||
|
Creates a new encrypted key store for API keys.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*KeyStore`: Key store instance
|
||||||
|
- `error`: Initialization error
|
||||||
|
|
||||||
|
### `(*KeyStore).StoreAPIKey(provider, apiKey string) error`
|
||||||
|
|
||||||
|
Securely stores an API key for a provider.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `provider`: AI service provider name
|
||||||
|
- `apiKey`: API key to store
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `error`: Storage error
|
||||||
|
|
||||||
|
**Security Features:**
|
||||||
|
- AES-GCM encryption
|
||||||
|
- Master password protection
|
||||||
|
- Secure file permissions (0600)
|
||||||
|
|
||||||
|
### `(*KeyStore).RetrieveAPIKey(provider string) (string, error)`
|
||||||
|
|
||||||
|
Retrieves and decrypts an API key for a provider.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `provider`: AI service provider name
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `string`: Decrypted API key
|
||||||
|
- `error`: Retrieval error
|
||||||
|
|
||||||
|
### `security.NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager`
|
||||||
|
|
||||||
|
Creates a new privilege manager for secure elevation.
|
||||||
|
|
||||||
|
### `(*PrivilegeManager).EscalateIfNeeded(args []string, operation string) error`
|
||||||
|
|
||||||
|
Escalates privileges using sudo if not already running as root.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `args`: Command arguments for elevated execution
|
||||||
|
- `operation`: Description of operation requiring elevation
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `error`: Escalation error
|
||||||
|
|
||||||
|
## Configuration API
|
||||||
|
|
||||||
|
The configuration module handles user preference gathering and processing.
|
||||||
|
|
||||||
|
### `config.GatherUserPreferences() (*types.UserPreferences, error)`
|
||||||
|
|
||||||
|
Interactively gathers user preferences for power management.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `*types.UserPreferences`: User preferences
|
||||||
|
- `error`: Gathering error
|
||||||
|
|
||||||
|
**Collected Preferences:**
|
||||||
|
- Power profile (balanced, performance, power saving, custom)
|
||||||
|
- Use case (general, development, gaming, server, multimedia, office)
|
||||||
|
- Battery priority (balanced, runtime, longevity)
|
||||||
|
- Performance mode (adaptive, maximum, efficient)
|
||||||
|
- Special requirements
|
||||||
|
- Custom settings
|
||||||
|
|
||||||
|
### `config.ValidatePreferences(preferences *types.UserPreferences) error`
|
||||||
|
|
||||||
|
Validates user preferences for consistency and conflicts.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `preferences`: Preferences to validate
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `error`: Validation error
|
||||||
|
|
||||||
|
### `config.GetRecommendedSettings(preferences *types.UserPreferences) map[string]string`
|
||||||
|
|
||||||
|
Generates recommended TLP settings based on user preferences.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `preferences`: User preferences
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `map[string]string`: Recommended TLP settings
|
||||||
|
|
||||||
|
## Types and Structures
|
||||||
|
|
||||||
|
### SystemInfo
|
||||||
|
|
||||||
|
Complete system information structure containing:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SystemInfo struct {
|
||||||
|
CPU CPUInfo `json:"cpu"`
|
||||||
|
Memory MemoryInfo `json:"memory"`
|
||||||
|
Battery *BatteryInfo `json:"battery,omitempty"`
|
||||||
|
PowerSupply PowerSupplyInfo `json:"power_supply"`
|
||||||
|
Kernel KernelInfo `json:"kernel"`
|
||||||
|
Distribution DistributionInfo `json:"distribution"`
|
||||||
|
Hardware HardwareInfo `json:"hardware"`
|
||||||
|
TLPStatus TLPStatus `json:"tlp_status"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UserPreferences
|
||||||
|
|
||||||
|
User power management preferences:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type UserPreferences struct {
|
||||||
|
PowerProfile PowerProfile `json:"power_profile"`
|
||||||
|
UseCase UseCase `json:"use_case"`
|
||||||
|
BatteryPriority BatteryPriority `json:"battery_priority"`
|
||||||
|
PerformanceMode PerformanceMode `json:"performance_mode"`
|
||||||
|
CustomSettings map[string]interface{} `json:"custom_settings"`
|
||||||
|
SpecialRequirements []string `json:"special_requirements"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLPConfiguration
|
||||||
|
|
||||||
|
Generated TLP configuration:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TLPConfiguration struct {
|
||||||
|
Settings map[string]string `json:"settings"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Rationale map[string]string `json:"rationale"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
Generated time.Time `json:"generated"`
|
||||||
|
SystemInfo *SystemInfo `json:"system_info"`
|
||||||
|
Preferences *UserPreferences `json:"preferences"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AIConfig
|
||||||
|
|
||||||
|
AI service configuration:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AIConfig struct {
|
||||||
|
Provider AIProvider `json:"provider"`
|
||||||
|
APIKey string `json:"-"` // Never serialized
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All APIs follow Go's standard error handling patterns:
|
||||||
|
|
||||||
|
- Functions return `error` as the last return value
|
||||||
|
- Errors are wrapped with context using `fmt.Errorf`
|
||||||
|
- Custom error types are used for specific error conditions
|
||||||
|
- Logging is performed at appropriate levels
|
||||||
|
|
||||||
|
## Context Usage
|
||||||
|
|
||||||
|
All long-running operations accept a `context.Context` parameter for:
|
||||||
|
|
||||||
|
- Cancellation support
|
||||||
|
- Timeout handling
|
||||||
|
- Request tracing
|
||||||
|
- Graceful shutdown
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
WiseTLP uses structured logging throughout:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := utils.NewLogger()
|
||||||
|
logger.Info("Operation completed", "duration", elapsed, "items", count)
|
||||||
|
logger.Error("Operation failed", "error", err, "context", ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
Log levels:
|
||||||
|
- `Debug`: Detailed debugging information
|
||||||
|
- `Info`: General operational information
|
||||||
|
- `Warn`: Warning conditions
|
||||||
|
- `Error`: Error conditions
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
All APIs include comprehensive unit tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run API tests
|
||||||
|
go test ./internal/...
|
||||||
|
go test ./pkg/...
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Test patterns:
|
||||||
|
- Table-driven tests for multiple scenarios
|
||||||
|
- Mock interfaces for external dependencies
|
||||||
|
- Integration tests for component interactions
|
||||||
|
- Error condition testing
|
134
examples/demo-config.sh
Normal file
134
examples/demo-config.sh
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# WiseTLP Demo Configuration Script
|
||||||
|
# This script demonstrates WiseTLP configuration with Groq API
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "WiseTLP Demo Configuration"
|
||||||
|
echo "=========================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [[ $EUID -eq 0 ]]; then
|
||||||
|
echo "This demo should not be run as root initially."
|
||||||
|
echo "WiseTLP will request elevation when needed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if autotlp binary exists
|
||||||
|
if [[ ! -f "./autotlp" ]]; then
|
||||||
|
echo "Building WiseTLP..."
|
||||||
|
go build -o autotlp ./cmd/autotlp
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Demo Configuration Settings:"
|
||||||
|
echo "- AI Provider: Groq"
|
||||||
|
echo "- API Endpoint: https://api.groq.com/openai/v1/chat/completions"
|
||||||
|
echo "- Model: openai/gpt-oss-20b"
|
||||||
|
echo "- Power Profile: Balanced"
|
||||||
|
echo "- Use Case: Development"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create a demo configuration file
|
||||||
|
cat > demo_responses.txt << 'EOF'
|
||||||
|
1
|
||||||
|
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
|
||||||
|
a
|
||||||
|
b
|
||||||
|
a
|
||||||
|
a
|
||||||
|
h
|
||||||
|
y
|
||||||
|
y
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Starting WiseTLP demo with pre-configured responses..."
|
||||||
|
echo "Note: This demo uses automated responses for demonstration purposes."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run WiseTLP with demo responses
|
||||||
|
# Note: In a real scenario, this would be interactive
|
||||||
|
echo "In a real scenario, WiseTLP would:"
|
||||||
|
echo "1. Detect your system specifications"
|
||||||
|
echo "2. Check TLP installation status"
|
||||||
|
echo "3. Configure AI service (Groq in this case)"
|
||||||
|
echo "4. Gather your power management preferences"
|
||||||
|
echo "5. Generate optimized TLP configuration using AI"
|
||||||
|
echo "6. Present the configuration for your approval"
|
||||||
|
echo "7. Apply the configuration to your system"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Example system detection output:"
|
||||||
|
echo "--------------------------------"
|
||||||
|
echo "Distribution: $(lsb_release -si 2>/dev/null || echo "Unknown")"
|
||||||
|
echo "Architecture: $(uname -m)"
|
||||||
|
echo "Kernel: $(uname -r)"
|
||||||
|
echo "CPU: $(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | xargs || echo "Unknown")"
|
||||||
|
echo "Memory: $(free -h | grep '^Mem:' | awk '{print $2}' || echo "Unknown")"
|
||||||
|
|
||||||
|
if [[ -d "/sys/class/power_supply/BAT0" ]]; then
|
||||||
|
echo "Battery: Present"
|
||||||
|
else
|
||||||
|
echo "Battery: Not detected (desktop system)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Example TLP configuration that would be generated:"
|
||||||
|
echo "------------------------------------------------"
|
||||||
|
cat << 'EOF'
|
||||||
|
# WiseTLP Generated Configuration
|
||||||
|
# Generated for Development workstation
|
||||||
|
# Power Profile: Balanced, Use Case: Development
|
||||||
|
|
||||||
|
TLP_ENABLE=1
|
||||||
|
TLP_WARN_LEVEL=3
|
||||||
|
|
||||||
|
# CPU scaling for development workloads
|
||||||
|
CPU_SCALING_GOVERNOR_ON_AC=ondemand
|
||||||
|
CPU_SCALING_GOVERNOR_ON_BAT=powersave
|
||||||
|
|
||||||
|
# Platform profiles
|
||||||
|
PLATFORM_PROFILE_ON_AC=balanced
|
||||||
|
PLATFORM_PROFILE_ON_BAT=low-power
|
||||||
|
|
||||||
|
# Disk management for development I/O
|
||||||
|
DISK_APM_LEVEL_ON_AC=254
|
||||||
|
DISK_APM_LEVEL_ON_BAT=192
|
||||||
|
|
||||||
|
# Network optimization
|
||||||
|
WIFI_PWR_ON_AC=off
|
||||||
|
WIFI_PWR_ON_BAT=on
|
||||||
|
|
||||||
|
# USB device management
|
||||||
|
USB_AUTOSUSPEND=1
|
||||||
|
USB_EXCLUDE_AUDIO=1
|
||||||
|
|
||||||
|
# Audio power management
|
||||||
|
SOUND_POWER_SAVE_ON_AC=1
|
||||||
|
SOUND_POWER_SAVE_ON_BAT=10
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "To run the actual WiseTLP application:"
|
||||||
|
echo " ./autotlp"
|
||||||
|
echo ""
|
||||||
|
echo "For testing without system changes:"
|
||||||
|
echo " ./autotlp --dry-run"
|
||||||
|
echo ""
|
||||||
|
echo "Demo completed! The actual WiseTLP provides:"
|
||||||
|
echo "- Interactive system detection"
|
||||||
|
echo "- AI-powered configuration generation"
|
||||||
|
echo "- Secure API key management"
|
||||||
|
echo "- Safe configuration validation"
|
||||||
|
echo "- Automatic TLP installation and setup"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f demo_responses.txt
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Visit the examples/ directory for more configuration samples:"
|
||||||
|
echo "- examples/gaming-laptop.md"
|
||||||
|
echo "- examples/development-workstation.md"
|
||||||
|
echo ""
|
||||||
|
echo "For full documentation, see README.md and docs/API.md"
|
382
examples/development-workstation.md
Normal file
382
examples/development-workstation.md
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
# Development Workstation Configuration Example
|
||||||
|
|
||||||
|
This example demonstrates WiseTLP configuration for a development workstation optimized for coding, compiling, and development workflows while maintaining energy efficiency.
|
||||||
|
|
||||||
|
## System Specifications
|
||||||
|
|
||||||
|
- **System**: Desktop workstation / High-end laptop
|
||||||
|
- **CPU**: AMD Ryzen 9 5900X (12 cores, 24 threads)
|
||||||
|
- **RAM**: 64GB DDR4-3200
|
||||||
|
- **Storage**: 2TB NVMe SSD (primary) + 4TB HDD (storage)
|
||||||
|
- **GPU**: NVIDIA RTX 3060 (for CUDA development)
|
||||||
|
- **Power**: 650W PSU / 100Wh battery (if laptop)
|
||||||
|
- **Display**: Dual 27" 4K monitors
|
||||||
|
|
||||||
|
## User Preferences
|
||||||
|
|
||||||
|
```
|
||||||
|
Power Profile: Balanced
|
||||||
|
Use Case: Development
|
||||||
|
Battery Priority: Runtime (if applicable)
|
||||||
|
Performance Mode: Adaptive
|
||||||
|
Special Requirements:
|
||||||
|
- Fast system wake/sleep
|
||||||
|
- Minimize fan noise during coding
|
||||||
|
- Optimize for external displays
|
||||||
|
- Maximum performance during compilation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated TLP Configuration
|
||||||
|
|
||||||
|
### General Settings
|
||||||
|
```bash
|
||||||
|
TLP_ENABLE=1
|
||||||
|
TLP_WARN_LEVEL=3
|
||||||
|
TLP_DEBUG=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### CPU Management
|
||||||
|
```bash
|
||||||
|
# Adaptive CPU scaling for development workloads
|
||||||
|
CPU_SCALING_GOVERNOR_ON_AC=ondemand
|
||||||
|
CPU_SCALING_GOVERNOR_ON_BAT=powersave
|
||||||
|
|
||||||
|
# Allow full frequency range for compilation
|
||||||
|
CPU_SCALING_MIN_FREQ_ON_AC=2200000
|
||||||
|
CPU_SCALING_MAX_FREQ_ON_AC=4950000
|
||||||
|
|
||||||
|
# Conservative battery frequencies
|
||||||
|
CPU_SCALING_MIN_FREQ_ON_BAT=2200000
|
||||||
|
CPU_SCALING_MAX_FREQ_ON_BAT=3600000
|
||||||
|
|
||||||
|
# Balanced platform profiles
|
||||||
|
PLATFORM_PROFILE_ON_AC=balanced
|
||||||
|
PLATFORM_PROFILE_ON_BAT=low-power
|
||||||
|
|
||||||
|
# Responsive energy performance
|
||||||
|
CPU_ENERGY_PERF_POLICY_ON_AC=balance_performance
|
||||||
|
CPU_ENERGY_PERF_POLICY_ON_BAT=balance_power
|
||||||
|
|
||||||
|
# Turbo boost for compilation workloads
|
||||||
|
CPU_BOOST_ON_AC=1
|
||||||
|
CPU_BOOST_ON_BAT=0
|
||||||
|
|
||||||
|
# Hardware P-states
|
||||||
|
CPU_HWP_DYN_BOOST_ON_AC=1
|
||||||
|
CPU_HWP_DYN_BOOST_ON_BAT=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Development work requires responsive performance for IDE operations and maximum performance for compilation. Ondemand governor provides good balance between responsiveness and power efficiency.
|
||||||
|
|
||||||
|
### Memory and Swap Management
|
||||||
|
```bash
|
||||||
|
# Optimize for large development environments
|
||||||
|
VM_DIRTY_WRITEBACK_CENTISECS_ON_AC=500
|
||||||
|
VM_DIRTY_WRITEBACK_CENTISECS_ON_BAT=1500
|
||||||
|
|
||||||
|
# Laptop mode for battery
|
||||||
|
VM_LAPTOP_MODE_ON_AC=0
|
||||||
|
VM_LAPTOP_MODE_ON_BAT=5
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Development environments often use significant memory. Balanced writeback settings ensure good I/O performance without excessive disk activity.
|
||||||
|
|
||||||
|
### Storage Management
|
||||||
|
```bash
|
||||||
|
# High performance for development tools and compilation
|
||||||
|
DISK_APM_LEVEL_ON_AC=254
|
||||||
|
DISK_APM_LEVEL_ON_BAT=192
|
||||||
|
|
||||||
|
# No spindown for SSDs, moderate for HDDs
|
||||||
|
DISK_SPINDOWN_TIMEOUT_ON_AC=0
|
||||||
|
DISK_SPINDOWN_TIMEOUT_ON_BAT=240
|
||||||
|
|
||||||
|
# Optimize SATA link power
|
||||||
|
SATA_LINKPWR_ON_AC=max_performance
|
||||||
|
SATA_LINKPWR_ON_BAT=medium_power
|
||||||
|
|
||||||
|
# NVMe optimization for development
|
||||||
|
AHCI_RUNTIME_PM_ON_AC=on
|
||||||
|
AHCI_RUNTIME_PM_ON_BAT=auto
|
||||||
|
|
||||||
|
# Disk device IDs for different handling
|
||||||
|
DISK_DEVICES="nvme0n1 sda"
|
||||||
|
DISK_APM_CLASS_DENYLIST="usb ieee1394"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Development requires fast I/O for IDE operations, file indexing, and compilation. SSD performance is prioritized while HDD is allowed moderate power saving.
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
```bash
|
||||||
|
# Reliable networking for remote development
|
||||||
|
WIFI_PWR_ON_AC=off
|
||||||
|
WIFI_PWR_ON_BAT=on
|
||||||
|
|
||||||
|
# Ethernet optimization
|
||||||
|
WOL_DISABLE=N
|
||||||
|
|
||||||
|
# Network device power management
|
||||||
|
RUNTIME_PM_DRIVER_DENYLIST="mei_me nouveau nvidia e1000e"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Development often requires reliable network connectivity for git operations, remote debugging, and API calls. Power saving on battery preserves runtime.
|
||||||
|
|
||||||
|
### GPU and Graphics
|
||||||
|
```bash
|
||||||
|
# NVIDIA GPU for CUDA development
|
||||||
|
RUNTIME_PM_ON_AC=auto
|
||||||
|
RUNTIME_PM_ON_BAT=auto
|
||||||
|
|
||||||
|
# Intel integrated graphics
|
||||||
|
INTEL_GPU_MIN_FREQ_ON_AC=300
|
||||||
|
INTEL_GPU_MAX_FREQ_ON_AC=1200
|
||||||
|
INTEL_GPU_MIN_FREQ_ON_BAT=300
|
||||||
|
INTEL_GPU_MAX_FREQ_ON_BAT=600
|
||||||
|
|
||||||
|
# External display optimization
|
||||||
|
RUNTIME_PM_DRIVER_DENYLIST="nvidia nouveau"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: CUDA development requires NVIDIA GPU availability. External displays need reliable graphics performance. Power management is balanced for development needs.
|
||||||
|
|
||||||
|
### USB and Peripheral Management
|
||||||
|
```bash
|
||||||
|
# Development peripherals optimization
|
||||||
|
USB_AUTOSUSPEND=1
|
||||||
|
USB_EXCLUDE_AUDIO=1
|
||||||
|
USB_EXCLUDE_BTUSB=1
|
||||||
|
USB_EXCLUDE_PHONE=1
|
||||||
|
|
||||||
|
# Exclude development devices
|
||||||
|
USB_ALLOWLIST="0403:6001 10c4:ea60" # FTDI and CP210x USB-to-serial
|
||||||
|
|
||||||
|
# Runtime PM exclusions
|
||||||
|
USB_EXCLUDE_WWAN=1
|
||||||
|
USB_EXCLUDE_PRINTER=1
|
||||||
|
|
||||||
|
# Autosuspend delays for development tools
|
||||||
|
USB_AUTOSUSPEND_DELAY=2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Development often involves USB devices like programmers, debuggers, and serial adapters. These need to remain active while other devices can power save.
|
||||||
|
|
||||||
|
### Audio and Multimedia
|
||||||
|
```bash
|
||||||
|
# Audio for development (video calls, notifications)
|
||||||
|
SOUND_POWER_SAVE_ON_AC=1
|
||||||
|
SOUND_POWER_SAVE_ON_BAT=10
|
||||||
|
SOUND_POWER_SAVE_CONTROLLER=Y
|
||||||
|
|
||||||
|
# Timeout for development environment
|
||||||
|
SOUND_POWER_SAVE_TIMEOUT=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Audio power saving with reasonable timeout for development notifications and video calls without impacting quality.
|
||||||
|
|
||||||
|
### Thermal and Fan Management
|
||||||
|
```bash
|
||||||
|
# Quiet operation for coding sessions
|
||||||
|
TEMP_LIMITS_ON_AC="75/85"
|
||||||
|
TEMP_LIMITS_ON_BAT="70/80"
|
||||||
|
|
||||||
|
# Fan speed control for quiet development
|
||||||
|
FAN_SPEED_ON_AC=auto
|
||||||
|
FAN_SPEED_ON_BAT=auto
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Lower thermal limits promote quieter fan operation during coding. Higher limits still allow performance during compilation.
|
||||||
|
|
||||||
|
### System Sleep and Power
|
||||||
|
```bash
|
||||||
|
# Fast wake for development workflow
|
||||||
|
RESTORE_DEVICE_STATE_ON_STARTUP=1
|
||||||
|
|
||||||
|
# Devices to keep configured
|
||||||
|
DEVICES_TO_DISABLE_ON_STARTUP=""
|
||||||
|
|
||||||
|
# Runtime PM for development
|
||||||
|
RUNTIME_PM_ALL=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Development workflow benefits from fast system wake/sleep cycles and maintaining device states for immediate productivity.
|
||||||
|
|
||||||
|
## Development-Specific Optimizations
|
||||||
|
|
||||||
|
### IDE and Editor Performance
|
||||||
|
```bash
|
||||||
|
# Filesystem optimizations for large codebases
|
||||||
|
VM_DIRTY_RATIO=15
|
||||||
|
VM_DIRTY_BACKGROUND_RATIO=5
|
||||||
|
|
||||||
|
# I/O scheduler optimization
|
||||||
|
DISK_IOSCHED="mq-deadline"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilation Workloads
|
||||||
|
```bash
|
||||||
|
# Temporary CPU boost for compilation
|
||||||
|
CPU_SCALING_GOVERNOR_ON_AC=performance # During make/ninja
|
||||||
|
CPU_SCALING_GOVERNOR_ON_AC=ondemand # Return to balanced
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container and Virtualization
|
||||||
|
```bash
|
||||||
|
# Docker/VM optimization
|
||||||
|
VM_SWAPPINESS=10
|
||||||
|
VM_VFS_CACHE_PRESSURE=50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
### Development Tasks Performance
|
||||||
|
- **IDE Startup**: Fast application launch with responsive UI
|
||||||
|
- **Code Indexing**: Efficient background processing
|
||||||
|
- **File Operations**: Quick file system operations for large projects
|
||||||
|
- **Git Operations**: Responsive version control operations
|
||||||
|
- **Compilation**: Maximum performance when needed
|
||||||
|
|
||||||
|
### Power Efficiency
|
||||||
|
- **Idle Power**: 15-25W desktop, 8-12W laptop
|
||||||
|
- **Coding Power**: 45-65W desktop, 15-25W laptop
|
||||||
|
- **Compilation Power**: 150-200W desktop, 45-65W laptop
|
||||||
|
- **Battery Runtime**: 8-12 hours coding, 3-5 hours compilation
|
||||||
|
|
||||||
|
## Workflow Optimization
|
||||||
|
|
||||||
|
### Morning Startup Routine
|
||||||
|
```bash
|
||||||
|
# System wake optimization
|
||||||
|
RESTORE_THINKPAD_BATTERY_THRESHOLDS=1
|
||||||
|
RESTORE_DEVICE_STATE_ON_STARTUP=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilation Sessions
|
||||||
|
```bash
|
||||||
|
# Temporary performance mode
|
||||||
|
echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||||
|
# Compile project
|
||||||
|
make -j$(nproc)
|
||||||
|
# Return to balanced
|
||||||
|
echo ondemand > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-of-day Shutdown
|
||||||
|
```bash
|
||||||
|
# Clean shutdown with state preservation
|
||||||
|
DEVICES_TO_DISABLE_ON_SHUTDOWN=""
|
||||||
|
USB_AUTOSUSPEND_DISABLE_ON_SHUTDOWN=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool-Specific Configurations
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
```bash
|
||||||
|
# Container runtime optimization
|
||||||
|
RUNTIME_PM_DRIVER_DENYLIST="docker0 br-*"
|
||||||
|
USB_EXCLUDE_PRINTER=1 # Prevent conflicts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote Development
|
||||||
|
```bash
|
||||||
|
# SSH and network optimization
|
||||||
|
WIFI_PWR_ON_AC=off
|
||||||
|
WIFI_PWR_ON_BAT=on
|
||||||
|
WOL_DISABLE=N
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Development
|
||||||
|
```bash
|
||||||
|
# I/O optimization for databases
|
||||||
|
DISK_APM_LEVEL_ON_AC=254
|
||||||
|
SATA_LINKPWR_ON_AC=max_performance
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Development
|
||||||
|
```bash
|
||||||
|
# Browser testing optimization
|
||||||
|
RUNTIME_PM_DRIVER_DENYLIST="nvidia nouveau" # For GPU acceleration
|
||||||
|
SOUND_POWER_SAVE_ON_AC=0 # For media testing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Optimization
|
||||||
|
|
||||||
|
### Development Metrics
|
||||||
|
```bash
|
||||||
|
# Monitor compilation performance
|
||||||
|
time make -j$(nproc)
|
||||||
|
|
||||||
|
# I/O performance for large projects
|
||||||
|
iotop -o
|
||||||
|
|
||||||
|
# Memory usage for IDEs
|
||||||
|
htop
|
||||||
|
|
||||||
|
# Network performance for remote work
|
||||||
|
iftop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Power Monitoring
|
||||||
|
```bash
|
||||||
|
# Real-time power consumption
|
||||||
|
sudo powertop --time=30
|
||||||
|
|
||||||
|
# Battery optimization
|
||||||
|
sudo tlp-stat -b
|
||||||
|
|
||||||
|
# Thermal monitoring during compilation
|
||||||
|
watch -n 1 sensors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
- **Slow compilation**: Check CPU governor and thermal throttling
|
||||||
|
- **IDE lag**: Monitor memory usage and I/O wait
|
||||||
|
- **Network issues**: Verify WiFi power management settings
|
||||||
|
|
||||||
|
### Power Issues
|
||||||
|
- **High idle power**: Check for runaway processes and USB devices
|
||||||
|
- **Poor battery life**: Review active background services
|
||||||
|
- **Thermal throttling**: Monitor temperatures during heavy workloads
|
||||||
|
|
||||||
|
### Development Environment Issues
|
||||||
|
- **USB device issues**: Check autosuspend settings for development tools
|
||||||
|
- **Display problems**: Verify graphics power management
|
||||||
|
- **Audio problems**: Check power save timeouts
|
||||||
|
|
||||||
|
## Custom Scripts
|
||||||
|
|
||||||
|
### Development Mode Toggle
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# dev-mode.sh - Toggle between development and power-save modes
|
||||||
|
|
||||||
|
if [ "$1" == "performance" ]; then
|
||||||
|
echo "Enabling development performance mode..."
|
||||||
|
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||||
|
echo 0 | sudo tee /sys/class/drm/card0/device/power_dpm_force_performance_level
|
||||||
|
elif [ "$1" == "balanced" ]; then
|
||||||
|
echo "Enabling balanced development mode..."
|
||||||
|
echo ondemand | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||||
|
echo auto | sudo tee /sys/class/drm/card0/device/power_dpm_force_performance_level
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilation Optimizer
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# compile-optimized.sh - Optimize system for compilation
|
||||||
|
|
||||||
|
echo "Optimizing for compilation..."
|
||||||
|
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||||
|
echo 254 | sudo tee /sys/class/scsi_host/host*/link_power_management_policy
|
||||||
|
|
||||||
|
# Run compilation
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
echo "Returning to balanced mode..."
|
||||||
|
echo ondemand | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||||
|
echo med_power_with_dipm | sudo tee /sys/class/scsi_host/host*/link_power_management_policy
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration provides optimal development performance while maintaining good power efficiency and system responsiveness for professional development workflows.
|
272
examples/gaming-laptop.md
Normal file
272
examples/gaming-laptop.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Gaming Laptop Configuration Example
|
||||||
|
|
||||||
|
This example demonstrates WiseTLP configuration for a gaming laptop optimized for performance while maintaining reasonable battery life during non-gaming use.
|
||||||
|
|
||||||
|
## System Specifications
|
||||||
|
|
||||||
|
- **Laptop**: Gaming laptop with dedicated GPU
|
||||||
|
- **CPU**: Intel Core i7-12700H (14 cores)
|
||||||
|
- **RAM**: 32GB DDR4
|
||||||
|
- **Storage**: 1TB NVMe SSD
|
||||||
|
- **GPU**: NVIDIA RTX 3070 Mobile
|
||||||
|
- **Battery**: 90Wh Li-ion
|
||||||
|
- **Display**: 15.6" 144Hz
|
||||||
|
|
||||||
|
## User Preferences
|
||||||
|
|
||||||
|
```
|
||||||
|
Power Profile: Performance
|
||||||
|
Use Case: Gaming
|
||||||
|
Battery Priority: Balanced
|
||||||
|
Performance Mode: Maximum
|
||||||
|
Special Requirements:
|
||||||
|
- Gaming performance priority
|
||||||
|
- Prevent thermal throttling
|
||||||
|
- Fast system wake/sleep
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated TLP Configuration
|
||||||
|
|
||||||
|
### General Settings
|
||||||
|
```bash
|
||||||
|
TLP_ENABLE=1
|
||||||
|
TLP_WARN_LEVEL=3
|
||||||
|
TLP_DEBUG=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### CPU Management
|
||||||
|
```bash
|
||||||
|
# Performance-oriented CPU scaling
|
||||||
|
CPU_SCALING_GOVERNOR_ON_AC=performance
|
||||||
|
CPU_SCALING_GOVERNOR_ON_BAT=ondemand
|
||||||
|
|
||||||
|
# Aggressive frequency scaling on AC
|
||||||
|
CPU_SCALING_MIN_FREQ_ON_AC=800000
|
||||||
|
CPU_SCALING_MAX_FREQ_ON_AC=4700000
|
||||||
|
|
||||||
|
# Conservative on battery to preserve power
|
||||||
|
CPU_SCALING_MIN_FREQ_ON_BAT=800000
|
||||||
|
CPU_SCALING_MAX_FREQ_ON_BAT=3200000
|
||||||
|
|
||||||
|
# Platform profiles for gaming performance
|
||||||
|
PLATFORM_PROFILE_ON_AC=performance
|
||||||
|
PLATFORM_PROFILE_ON_BAT=balanced
|
||||||
|
|
||||||
|
# CPU energy performance preference
|
||||||
|
CPU_ENERGY_PERF_POLICY_ON_AC=performance
|
||||||
|
CPU_ENERGY_PERF_POLICY_ON_BAT=balance_performance
|
||||||
|
|
||||||
|
# Turbo boost settings
|
||||||
|
CPU_BOOST_ON_AC=1
|
||||||
|
CPU_BOOST_ON_BAT=1
|
||||||
|
|
||||||
|
# HWP (Hardware P-states) settings
|
||||||
|
CPU_HWP_DYN_BOOST_ON_AC=1
|
||||||
|
CPU_HWP_DYN_BOOST_ON_BAT=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Gaming requires maximum CPU performance on AC power. On battery, we use ondemand governor to balance performance with battery life while still allowing turbo boost for responsiveness.
|
||||||
|
|
||||||
|
### GPU Power Management
|
||||||
|
```bash
|
||||||
|
# NVIDIA GPU settings
|
||||||
|
RUNTIME_PM_ON_AC=auto
|
||||||
|
RUNTIME_PM_ON_BAT=auto
|
||||||
|
|
||||||
|
# Radeon DPM (if applicable)
|
||||||
|
RADEON_DPM_STATE_ON_AC=performance
|
||||||
|
RADEON_DPM_STATE_ON_BAT=balanced
|
||||||
|
|
||||||
|
# Intel GPU settings
|
||||||
|
INTEL_GPU_MIN_FREQ_ON_AC=300
|
||||||
|
INTEL_GPU_MAX_FREQ_ON_AC=1300
|
||||||
|
INTEL_GPU_MIN_FREQ_ON_BAT=300
|
||||||
|
INTEL_GPU_MAX_FREQ_ON_BAT=800
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Dedicated GPU performance is prioritized on AC power for gaming. Battery mode reduces GPU frequency to conserve power.
|
||||||
|
|
||||||
|
### Disk Management
|
||||||
|
```bash
|
||||||
|
# Aggressive disk performance on AC
|
||||||
|
DISK_APM_LEVEL_ON_AC=254
|
||||||
|
DISK_APM_LEVEL_ON_BAT=128
|
||||||
|
|
||||||
|
# No spindown timeout for SSD
|
||||||
|
DISK_SPINDOWN_TIMEOUT_ON_AC=0
|
||||||
|
DISK_SPINDOWN_TIMEOUT_ON_BAT=0
|
||||||
|
|
||||||
|
# SATA link power management
|
||||||
|
SATA_LINKPWR_ON_AC=max_performance
|
||||||
|
SATA_LINKPWR_ON_BAT=med_power_with_dipm
|
||||||
|
|
||||||
|
# NVMe power management
|
||||||
|
AHCI_RUNTIME_PM_ON_AC=on
|
||||||
|
AHCI_RUNTIME_PM_ON_BAT=auto
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Fast game loading requires maximum disk performance on AC. SSD doesn't need spindown timeout. Battery mode uses moderate power saving.
|
||||||
|
|
||||||
|
### Network Management
|
||||||
|
```bash
|
||||||
|
# WiFi power management
|
||||||
|
WIFI_PWR_ON_AC=off
|
||||||
|
WIFI_PWR_ON_BAT=on
|
||||||
|
|
||||||
|
# Ethernet wake-on-LAN
|
||||||
|
WOL_DISABLE=Y
|
||||||
|
|
||||||
|
# Network device power management
|
||||||
|
RUNTIME_PM_DRIVER_DENYLIST="mei_me nouveau nvidia"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Gaming requires reliable, low-latency network connectivity. WiFi power saving is disabled on AC but enabled on battery to conserve power.
|
||||||
|
|
||||||
|
### USB and Peripheral Management
|
||||||
|
```bash
|
||||||
|
# USB autosuspend for power saving
|
||||||
|
USB_AUTOSUSPEND=1
|
||||||
|
USB_EXCLUDE_AUDIO=1
|
||||||
|
USB_EXCLUDE_BTUSB=1
|
||||||
|
USB_EXCLUDE_PHONE=1
|
||||||
|
|
||||||
|
# Exclude gaming peripherals from autosuspend
|
||||||
|
USB_ALLOWLIST="046d:c52b 1532:0037" # Gaming mouse and keyboard
|
||||||
|
|
||||||
|
# Runtime PM for USB devices
|
||||||
|
USB_AUTOSUSPEND_DISABLE_ON_SHUTDOWN=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Gaming peripherals need to remain active for immediate response. Audio and Bluetooth devices are excluded from autosuspend to prevent interruptions.
|
||||||
|
|
||||||
|
### Thermal Management
|
||||||
|
```bash
|
||||||
|
# Thermal zone trip points (prevent throttling)
|
||||||
|
TEMP_LIMITS_ON_AC="85/95"
|
||||||
|
TEMP_LIMITS_ON_BAT="80/90"
|
||||||
|
|
||||||
|
# Fan control (if supported)
|
||||||
|
FAN_SPEED_ON_AC=auto
|
||||||
|
FAN_SPEED_ON_BAT=auto
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Higher thermal limits on AC power allow for sustained gaming performance. Battery limits are lower to prevent overheating during mobile use.
|
||||||
|
|
||||||
|
### Audio and Multimedia
|
||||||
|
```bash
|
||||||
|
# Audio power management
|
||||||
|
SOUND_POWER_SAVE_ON_AC=0
|
||||||
|
SOUND_POWER_SAVE_ON_BAT=1
|
||||||
|
SOUND_POWER_SAVE_CONTROLLER=Y
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Audio power saving disabled on AC for gaming audio quality. Enabled on battery for power conservation.
|
||||||
|
|
||||||
|
### System Sleep and Wake
|
||||||
|
```bash
|
||||||
|
# Fast resume for gaming
|
||||||
|
RESTORE_DEVICE_STATE_ON_STARTUP=1
|
||||||
|
DEVICES_TO_DISABLE_ON_STARTUP=""
|
||||||
|
|
||||||
|
# USB device handling during sleep
|
||||||
|
USB_EXCLUDE_WWAN=1
|
||||||
|
USB_EXCLUDE_PRINTER=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Fast wake/sleep cycle for gaming sessions. Critical devices remain configured for immediate use.
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
|
||||||
|
### Gaming Performance (AC Power)
|
||||||
|
- **CPU**: Maximum performance with all cores available
|
||||||
|
- **GPU**: Full performance mode for dedicated graphics
|
||||||
|
- **Storage**: Maximum I/O performance for fast game loading
|
||||||
|
- **Network**: Low-latency connectivity for online gaming
|
||||||
|
- **Thermal**: Higher limits to prevent throttling during intensive gaming
|
||||||
|
|
||||||
|
### Battery Life (Battery Power)
|
||||||
|
- **Runtime**: 4-6 hours for general use, 1.5-2.5 hours for gaming
|
||||||
|
- **Standby**: Optimized sleep/wake for extended standby time
|
||||||
|
- **Balanced**: Performance available when needed, power saving when idle
|
||||||
|
|
||||||
|
## Warnings and Considerations
|
||||||
|
|
||||||
|
1. **Thermal Management**: Higher performance settings may increase heat generation. Ensure adequate cooling.
|
||||||
|
|
||||||
|
2. **Battery Health**: Gaming on battery will reduce battery lifespan. Consider AC power for extended gaming sessions.
|
||||||
|
|
||||||
|
3. **Fan Noise**: Performance mode may result in more aggressive fan curves for thermal management.
|
||||||
|
|
||||||
|
4. **Power Consumption**: AC power consumption will be higher during gaming sessions (150-200W typical).
|
||||||
|
|
||||||
|
5. **Component Longevity**: Sustained high performance may impact long-term component reliability.
|
||||||
|
|
||||||
|
## Customization Options
|
||||||
|
|
||||||
|
### For Competitive Gaming
|
||||||
|
```bash
|
||||||
|
# Even more aggressive performance
|
||||||
|
CPU_SCALING_GOVERNOR_ON_BAT=performance
|
||||||
|
PLATFORM_PROFILE_ON_BAT=performance
|
||||||
|
WIFI_PWR_ON_BAT=off
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Extended Battery Gaming
|
||||||
|
```bash
|
||||||
|
# More conservative battery settings
|
||||||
|
CPU_SCALING_MAX_FREQ_ON_BAT=2400000
|
||||||
|
CPU_BOOST_ON_BAT=0
|
||||||
|
PLATFORM_PROFILE_ON_BAT=low-power
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Content Creation
|
||||||
|
```bash
|
||||||
|
# Optimize for rendering workloads
|
||||||
|
CPU_ENERGY_PERF_POLICY_ON_AC=performance
|
||||||
|
CPU_ENERGY_PERF_POLICY_ON_BAT=performance
|
||||||
|
SOUND_POWER_SAVE_ON_AC=0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Validation
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
```bash
|
||||||
|
# CPU frequency monitoring
|
||||||
|
watch -n 1 'cat /proc/cpuinfo | grep MHz'
|
||||||
|
|
||||||
|
# Thermal monitoring
|
||||||
|
watch -n 1 'sensors'
|
||||||
|
|
||||||
|
# Power consumption
|
||||||
|
sudo powertop --time=60
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gaming Benchmarks
|
||||||
|
- **3DMark**: Graphics performance validation
|
||||||
|
- **Unigine Heaven**: Thermal stability testing
|
||||||
|
- **CPU-Z**: CPU performance verification
|
||||||
|
- **CrystalDiskMark**: Storage performance testing
|
||||||
|
|
||||||
|
### Battery Testing
|
||||||
|
- **PowerTOP**: Power consumption analysis
|
||||||
|
- **Battery runtime**: Real-world usage testing
|
||||||
|
- **Standby drain**: Sleep power consumption
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
- Verify CPU governor is set correctly: `cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor`
|
||||||
|
- Check thermal throttling: `dmesg | grep -i thermal`
|
||||||
|
- Monitor GPU performance: `nvidia-smi` (for NVIDIA)
|
||||||
|
|
||||||
|
### Battery Issues
|
||||||
|
- Check power consumption: `sudo powertop`
|
||||||
|
- Verify power profiles: `system76-power profile`
|
||||||
|
- Monitor charging behavior: `upower -i /org/freedesktop/UPower/devices/battery_BAT0`
|
||||||
|
|
||||||
|
### Thermal Issues
|
||||||
|
- Monitor temperatures: `sensors`
|
||||||
|
- Check fan operation: `pwmconfig` (if supported)
|
||||||
|
- Verify thermal limits: Check BIOS/UEFI settings
|
||||||
|
|
||||||
|
This configuration provides optimal gaming performance while maintaining reasonable power management for non-gaming use cases.
|
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require golang.org/x/term v0.24.0
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.25.0 // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||||
|
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
347
internal/ai/client.go
Normal file
347
internal/ai/client.go
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/config"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
config *types.AIConfig
|
||||||
|
httpClient *http.Client
|
||||||
|
logger *utils.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(config *types.AIConfig, logger *utils.Logger) *Client {
|
||||||
|
return &Client{
|
||||||
|
config: config,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
logger: logger.WithComponent("ai"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigureClient(logger *utils.Logger) (*Client, error) {
|
||||||
|
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||||
|
fmt.Println("AI SERVICE CONFIGURATION")
|
||||||
|
fmt.Println(strings.Repeat("=", 50))
|
||||||
|
|
||||||
|
config := &types.AIConfig{}
|
||||||
|
|
||||||
|
fmt.Println("\nAvailable AI providers:")
|
||||||
|
fmt.Println("1. Groq (Fast inference)")
|
||||||
|
fmt.Println("2. OpenRouter (Multiple models)")
|
||||||
|
fmt.Println("3. Gemini (Google)")
|
||||||
|
fmt.Println("4. Custom (OpenAI-compatible)")
|
||||||
|
|
||||||
|
choice := utils.GetUserInput("Select provider (1-4)", "1")
|
||||||
|
|
||||||
|
switch choice {
|
||||||
|
case "1":
|
||||||
|
config.Provider = types.AIProviderGroq
|
||||||
|
config.Endpoint = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
|
config.Model = "openai/gpt-oss-20b"
|
||||||
|
case "2":
|
||||||
|
config.Provider = types.AIProviderOpenRouter
|
||||||
|
config.Endpoint = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
config.Model = utils.GetUserInput("Model name", "meta-llama/llama-3.1-8b-instruct:free")
|
||||||
|
case "3":
|
||||||
|
config.Provider = types.AIProviderGemini
|
||||||
|
config.Endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"
|
||||||
|
config.Model = "gemini-pro"
|
||||||
|
case "4":
|
||||||
|
config.Provider = types.AIProviderCustom
|
||||||
|
config.Endpoint = utils.GetUserInput("API endpoint", "")
|
||||||
|
config.Model = utils.GetUserInput("Model name", "")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid provider selection")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nEnter your API key for %s: ", config.Provider)
|
||||||
|
var apiKey string
|
||||||
|
fmt.Scanln(&apiKey)
|
||||||
|
config.APIKey = strings.TrimSpace(apiKey)
|
||||||
|
|
||||||
|
if config.APIKey == "" {
|
||||||
|
return nil, fmt.Errorf("API key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.MaxTokens = 1500
|
||||||
|
config.Temperature = 0.3
|
||||||
|
|
||||||
|
client := NewClient(config, logger)
|
||||||
|
if err := client.validateConnection(context.Background()); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to validate AI connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✓ AI service configured successfully!")
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GenerateConfig(ctx context.Context, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
|
||||||
|
c.logger.Info("Generating TLP configuration using AI", "provider", c.config.Provider, "model", c.config.Model)
|
||||||
|
|
||||||
|
prompt := c.buildPrompt(sysInfo, preferences)
|
||||||
|
response, err := c.makeRequest(ctx, prompt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AI request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := c.parseResponse(response, sysInfo, preferences)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse AI response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Info("TLP configuration generated successfully", "settings_count", len(config.Settings))
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) buildPrompt(sysInfo *types.SystemInfo, preferences *types.UserPreferences) string {
|
||||||
|
var prompt strings.Builder
|
||||||
|
|
||||||
|
prompt.WriteString("Generate TLP configuration in JSON format.\n\n")
|
||||||
|
|
||||||
|
prompt.WriteString("System: ")
|
||||||
|
if sysInfo.Battery != nil && sysInfo.Battery.Present {
|
||||||
|
prompt.WriteString("Laptop")
|
||||||
|
} else {
|
||||||
|
prompt.WriteString("Desktop")
|
||||||
|
}
|
||||||
|
prompt.WriteString(fmt.Sprintf(", %s, %d cores\n", sysInfo.Distribution.ID, sysInfo.CPU.Cores))
|
||||||
|
|
||||||
|
prompt.WriteString(fmt.Sprintf("Profile: %s, Use: %s, Mode: %s\n\n", preferences.PowerProfile, preferences.UseCase, preferences.PerformanceMode))
|
||||||
|
|
||||||
|
prompt.WriteString("Return JSON with this structure:\n")
|
||||||
|
prompt.WriteString(`{"description": "Config description", "settings": {"TLP_ENABLE": "1", "CPU_SCALING_GOVERNOR_ON_AC": "performance", "CPU_SCALING_GOVERNOR_ON_BAT": "powersave", "DISK_APM_LEVEL_ON_AC": "254", "DISK_APM_LEVEL_ON_BAT": "128", "WIFI_PWR_ON_AC": "off", "WIFI_PWR_ON_BAT": "on", "USB_AUTOSUSPEND": "1"}, "rationale": {"CPU_SCALING_GOVERNOR_ON_AC": "Max performance on AC"}, "warnings": ["Monitor temperatures"]}` + "\n\n")
|
||||||
|
|
||||||
|
prompt.WriteString("Generate 8-10 TLP settings for this system.")
|
||||||
|
|
||||||
|
return prompt.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) makeRequest(ctx context.Context, prompt string) (string, error) {
|
||||||
|
c.logger.Debug("Making AI request", "prompt_length", len(prompt))
|
||||||
|
c.logger.Debug("Full prompt being sent", "prompt", prompt)
|
||||||
|
|
||||||
|
var requestBody interface{}
|
||||||
|
var endpoint string
|
||||||
|
|
||||||
|
switch c.config.Provider {
|
||||||
|
case types.AIProviderGemini:
|
||||||
|
requestBody = map[string]interface{}{
|
||||||
|
"contents": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"parts": []map[string]string{
|
||||||
|
{"text": prompt},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"generationConfig": map[string]interface{}{
|
||||||
|
"temperature": c.config.Temperature,
|
||||||
|
"maxOutputTokens": c.config.MaxTokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
endpoint = c.config.Endpoint + "?key=" + c.config.APIKey
|
||||||
|
default:
|
||||||
|
requestBody = map[string]interface{}{
|
||||||
|
"model": c.config.Model,
|
||||||
|
"messages": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"max_tokens": c.config.MaxTokens,
|
||||||
|
"temperature": c.config.Temperature,
|
||||||
|
"response_format": map[string]interface{}{
|
||||||
|
"type": "json_object",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
endpoint = c.config.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBody, err := json.Marshal(requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Debug("Request body", "json", string(jsonBody))
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonBody))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if c.config.Provider != types.AIProviderGemini {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.extractContent(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) extractContent(responseBody []byte) (string, error) {
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.Unmarshal(responseBody, &response); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse response JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.config.Provider {
|
||||||
|
case types.AIProviderGemini:
|
||||||
|
if candidates, ok := response["candidates"].([]interface{}); ok && len(candidates) > 0 {
|
||||||
|
if candidate, ok := candidates[0].(map[string]interface{}); ok {
|
||||||
|
if content, ok := candidate["content"].(map[string]interface{}); ok {
|
||||||
|
if parts, ok := content["parts"].([]interface{}); ok && len(parts) > 0 {
|
||||||
|
if part, ok := parts[0].(map[string]interface{}); ok {
|
||||||
|
if text, ok := part["text"].(string); ok {
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if choices, ok := response["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||||
|
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||||
|
if message, ok := choice["message"].(map[string]interface{}); ok {
|
||||||
|
if content, ok := message["content"].(string); ok {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("unexpected response format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) parseResponse(response string, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
|
||||||
|
c.logger.Debug("Raw AI response", "response", response)
|
||||||
|
|
||||||
|
start := strings.Index(response, "{")
|
||||||
|
end := strings.LastIndex(response, "}") + 1
|
||||||
|
|
||||||
|
if start == -1 || end == 0 {
|
||||||
|
c.logger.Error("No JSON found in AI response", "response_length", len(response), "response_preview", response[:min(200, len(response))])
|
||||||
|
c.logger.Info("Generating fallback TLP configuration")
|
||||||
|
return c.generateFallbackConfig(sysInfo, preferences), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr := response[start:end]
|
||||||
|
|
||||||
|
var aiResponse struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Settings map[string]string `json:"settings"`
|
||||||
|
Rationale map[string]string `json:"rationale"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &aiResponse); err != nil {
|
||||||
|
c.logger.Error("Failed to parse AI JSON response", "error", err, "json_str", jsonStr)
|
||||||
|
c.logger.Info("Generating fallback TLP configuration due to JSON parse error")
|
||||||
|
return c.generateFallbackConfig(sysInfo, preferences), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aiResponse.Settings) == 0 {
|
||||||
|
return nil, fmt.Errorf("AI response contains no settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &types.TLPConfiguration{
|
||||||
|
Settings: aiResponse.Settings,
|
||||||
|
Description: aiResponse.Description,
|
||||||
|
Rationale: aiResponse.Rationale,
|
||||||
|
Warnings: aiResponse.Warnings,
|
||||||
|
Generated: time.Now(),
|
||||||
|
SystemInfo: sysInfo,
|
||||||
|
Preferences: preferences,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := config.Settings["TLP_ENABLE"]; !exists {
|
||||||
|
config.Settings["TLP_ENABLE"] = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Rationale == nil {
|
||||||
|
config.Rationale = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) validateConnection(ctx context.Context) error {
|
||||||
|
testPrompt := "Respond with a JSON object containing 'status': 'OK' to confirm the connection is working."
|
||||||
|
|
||||||
|
response, err := c.makeRequest(ctx, testPrompt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connection test failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(strings.ToUpper(response), "OK") {
|
||||||
|
return fmt.Errorf("unexpected response from AI service: %s", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) generateFallbackConfig(sysInfo *types.SystemInfo, preferences *types.UserPreferences) *types.TLPConfiguration {
|
||||||
|
c.logger.Info("Generating fallback configuration based on user preferences")
|
||||||
|
|
||||||
|
settings := config.GetRecommendedSettings(preferences)
|
||||||
|
|
||||||
|
rationale := make(map[string]string)
|
||||||
|
rationale["TLP_ENABLE"] = "Enable TLP power management"
|
||||||
|
rationale["CPU_SCALING_GOVERNOR_ON_AC"] = "CPU governor for AC power based on user preferences"
|
||||||
|
rationale["CPU_SCALING_GOVERNOR_ON_BAT"] = "CPU governor for battery power based on user preferences"
|
||||||
|
|
||||||
|
warnings := []string{
|
||||||
|
"This is a fallback configuration generated when AI service was unavailable",
|
||||||
|
"Configuration is based on user preferences and common best practices",
|
||||||
|
"Consider running WiseTLP again when AI service is available for optimized settings",
|
||||||
|
}
|
||||||
|
|
||||||
|
description := fmt.Sprintf("Fallback TLP configuration for %s use case with %s power profile",
|
||||||
|
preferences.UseCase, preferences.PowerProfile)
|
||||||
|
|
||||||
|
return &types.TLPConfiguration{
|
||||||
|
Settings: settings,
|
||||||
|
Description: description,
|
||||||
|
Rationale: rationale,
|
||||||
|
Warnings: warnings,
|
||||||
|
Generated: time.Now(),
|
||||||
|
SystemInfo: sysInfo,
|
||||||
|
Preferences: preferences,
|
||||||
|
}
|
||||||
|
}
|
346
internal/config/preferences.go
Normal file
346
internal/config/preferences.go
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration
|
||||||
|
type Config struct {
|
||||||
|
// Application-wide configuration settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new configuration instance
|
||||||
|
func New() *Config {
|
||||||
|
return &Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GatherUserPreferences interactively collects user preferences
|
||||||
|
func GatherUserPreferences() (*types.UserPreferences, error) {
|
||||||
|
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||||
|
fmt.Println("USER PREFERENCES CONFIGURATION")
|
||||||
|
fmt.Println(strings.Repeat("=", 60))
|
||||||
|
|
||||||
|
preferences := &types.UserPreferences{
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power Profile
|
||||||
|
fmt.Println("\n1. Power Profile Selection")
|
||||||
|
fmt.Println("Choose your preferred power management approach:")
|
||||||
|
fmt.Println(" a) Balanced - Good compromise between performance and power saving")
|
||||||
|
fmt.Println(" b) Performance - Maximum performance, higher power consumption")
|
||||||
|
fmt.Println(" c) Power Saving - Maximum battery life, reduced performance")
|
||||||
|
fmt.Println(" d) Custom - I'll specify custom requirements")
|
||||||
|
|
||||||
|
profileChoice := utils.GetUserInput("Select power profile (a/b/c/d)", "a")
|
||||||
|
switch strings.ToLower(profileChoice) {
|
||||||
|
case "a", "balanced":
|
||||||
|
preferences.PowerProfile = types.PowerProfileBalanced
|
||||||
|
case "b", "performance":
|
||||||
|
preferences.PowerProfile = types.PowerProfilePerformance
|
||||||
|
case "c", "power saving", "powersaving":
|
||||||
|
preferences.PowerProfile = types.PowerProfilePowerSaving
|
||||||
|
case "d", "custom":
|
||||||
|
preferences.PowerProfile = types.PowerProfileCustom
|
||||||
|
default:
|
||||||
|
preferences.PowerProfile = types.PowerProfileBalanced
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Case
|
||||||
|
fmt.Println("\n2. Primary Use Case")
|
||||||
|
fmt.Println("What do you primarily use this system for?")
|
||||||
|
fmt.Println(" a) General use - Web browsing, office work, media consumption")
|
||||||
|
fmt.Println(" b) Development - Programming, compiling, development tools")
|
||||||
|
fmt.Println(" c) Gaming - Gaming and graphics-intensive applications")
|
||||||
|
fmt.Println(" d) Server - Server workloads, always-on services")
|
||||||
|
fmt.Println(" e) Multimedia - Video editing, rendering, content creation")
|
||||||
|
fmt.Println(" f) Office - Primarily office applications and productivity")
|
||||||
|
|
||||||
|
useCaseChoice := utils.GetUserInput("Select use case (a/b/c/d/e/f)", "a")
|
||||||
|
switch strings.ToLower(useCaseChoice) {
|
||||||
|
case "a", "general":
|
||||||
|
preferences.UseCase = types.UseCaseGeneral
|
||||||
|
case "b", "development", "dev":
|
||||||
|
preferences.UseCase = types.UseCaseDevelopment
|
||||||
|
case "c", "gaming":
|
||||||
|
preferences.UseCase = types.UseCaseGaming
|
||||||
|
case "d", "server":
|
||||||
|
preferences.UseCase = types.UseCaseServer
|
||||||
|
case "e", "multimedia", "media":
|
||||||
|
preferences.UseCase = types.UseCaseMultimedia
|
||||||
|
case "f", "office":
|
||||||
|
preferences.UseCase = types.UseCaseOffice
|
||||||
|
default:
|
||||||
|
preferences.UseCase = types.UseCaseGeneral
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery Priority (only for systems with batteries)
|
||||||
|
fmt.Println("\n3. Battery Optimization Priority")
|
||||||
|
fmt.Println("How would you like to optimize battery usage?")
|
||||||
|
fmt.Println(" a) Balanced - Balance between battery life and longevity")
|
||||||
|
fmt.Println(" b) Runtime - Maximize single-charge runtime")
|
||||||
|
fmt.Println(" c) Longevity - Maximize battery lifespan over years")
|
||||||
|
|
||||||
|
batteryChoice := utils.GetUserInput("Select battery priority (a/b/c)", "a")
|
||||||
|
switch strings.ToLower(batteryChoice) {
|
||||||
|
case "a", "balanced":
|
||||||
|
preferences.BatteryPriority = types.BatteryPriorityBalanced
|
||||||
|
case "b", "runtime":
|
||||||
|
preferences.BatteryPriority = types.BatteryPriorityRuntime
|
||||||
|
case "c", "longevity":
|
||||||
|
preferences.BatteryPriority = types.BatteryPriorityLongevity
|
||||||
|
default:
|
||||||
|
preferences.BatteryPriority = types.BatteryPriorityBalanced
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance Mode
|
||||||
|
fmt.Println("\n4. Performance Mode")
|
||||||
|
fmt.Println("How should the system handle performance scaling?")
|
||||||
|
fmt.Println(" a) Adaptive - Automatically adjust based on workload")
|
||||||
|
fmt.Println(" b) Maximum - Always prefer maximum performance")
|
||||||
|
fmt.Println(" c) Efficient - Always prefer energy efficiency")
|
||||||
|
|
||||||
|
perfChoice := utils.GetUserInput("Select performance mode (a/b/c)", "a")
|
||||||
|
switch strings.ToLower(perfChoice) {
|
||||||
|
case "a", "adaptive":
|
||||||
|
preferences.PerformanceMode = types.PerformanceModeAdaptive
|
||||||
|
case "b", "maximum", "max":
|
||||||
|
preferences.PerformanceMode = types.PerformanceModeMaximum
|
||||||
|
case "c", "efficient", "efficiency":
|
||||||
|
preferences.PerformanceMode = types.PerformanceModeEfficient
|
||||||
|
default:
|
||||||
|
preferences.PerformanceMode = types.PerformanceModeAdaptive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Requirements
|
||||||
|
fmt.Println("\n5. Special Requirements")
|
||||||
|
fmt.Println("Do you have any special requirements? (Select all that apply)")
|
||||||
|
fmt.Println(" a) Minimize fan noise")
|
||||||
|
fmt.Println(" b) Prevent thermal throttling")
|
||||||
|
fmt.Println(" c) Optimize for external displays")
|
||||||
|
fmt.Println(" d) Gaming performance priority")
|
||||||
|
fmt.Println(" e) Maximum WiFi performance")
|
||||||
|
fmt.Println(" f) Minimize disk wear")
|
||||||
|
fmt.Println(" g) Fast system wake/sleep")
|
||||||
|
fmt.Println(" h) None of the above")
|
||||||
|
|
||||||
|
requirements := utils.GetUserInput("Enter letters separated by spaces (e.g., 'a c f')", "h")
|
||||||
|
if strings.ToLower(requirements) != "h" {
|
||||||
|
reqMap := map[string]string{
|
||||||
|
"a": "Minimize fan noise",
|
||||||
|
"b": "Prevent thermal throttling",
|
||||||
|
"c": "Optimize for external displays",
|
||||||
|
"d": "Gaming performance priority",
|
||||||
|
"e": "Maximum WiFi performance",
|
||||||
|
"f": "Minimize disk wear",
|
||||||
|
"g": "Fast system wake/sleep",
|
||||||
|
}
|
||||||
|
|
||||||
|
reqLetters := strings.Fields(strings.ToLower(requirements))
|
||||||
|
for _, letter := range reqLetters {
|
||||||
|
if req, exists := reqMap[letter]; exists {
|
||||||
|
preferences.SpecialRequirements = append(preferences.SpecialRequirements, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Settings (for advanced users)
|
||||||
|
if preferences.PowerProfile == types.PowerProfileCustom {
|
||||||
|
fmt.Println("\n6. Custom Settings")
|
||||||
|
fmt.Println("You selected custom power profile. You can specify additional settings.")
|
||||||
|
|
||||||
|
if utils.GetUserConfirmation("Do you want to specify custom CPU governor settings?") {
|
||||||
|
acGovernor := utils.GetUserInput("CPU governor on AC power (performance/powersave/ondemand/conservative/schedutil)", "performance")
|
||||||
|
batGovernor := utils.GetUserInput("CPU governor on battery (performance/powersave/ondemand/conservative/schedutil)", "powersave")
|
||||||
|
|
||||||
|
preferences.CustomSettings["cpu_governor_ac"] = acGovernor
|
||||||
|
preferences.CustomSettings["cpu_governor_battery"] = batGovernor
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.GetUserConfirmation("Do you want to specify custom disk settings?") {
|
||||||
|
diskAPMAC := utils.GetUserInput("Disk APM level on AC (1-255, higher = more aggressive)", "254")
|
||||||
|
diskAPMBat := utils.GetUserInput("Disk APM level on battery (1-255, higher = more aggressive)", "128")
|
||||||
|
|
||||||
|
preferences.CustomSettings["disk_apm_ac"] = diskAPMAC
|
||||||
|
preferences.CustomSettings["disk_apm_battery"] = diskAPMBat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
fmt.Println("\n" + strings.Repeat("-", 60))
|
||||||
|
fmt.Println("PREFERENCES SUMMARY")
|
||||||
|
fmt.Println(strings.Repeat("-", 60))
|
||||||
|
fmt.Printf("Power Profile: %s\n", preferences.PowerProfile)
|
||||||
|
fmt.Printf("Use Case: %s\n", preferences.UseCase)
|
||||||
|
fmt.Printf("Battery Priority: %s\n", preferences.BatteryPriority)
|
||||||
|
fmt.Printf("Performance Mode: %s\n", preferences.PerformanceMode)
|
||||||
|
|
||||||
|
if len(preferences.SpecialRequirements) > 0 {
|
||||||
|
fmt.Printf("Special Requirements: %s\n", strings.Join(preferences.SpecialRequirements, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(preferences.CustomSettings) > 0 {
|
||||||
|
fmt.Println("Custom Settings:")
|
||||||
|
for key, value := range preferences.CustomSettings {
|
||||||
|
fmt.Printf(" %s: %v\n", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(strings.Repeat("-", 60))
|
||||||
|
|
||||||
|
if !utils.GetUserConfirmation("Are these preferences correct?") {
|
||||||
|
fmt.Println("Let's try again...")
|
||||||
|
return GatherUserPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
return preferences, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePreferences validates user preferences for consistency
|
||||||
|
func ValidatePreferences(preferences *types.UserPreferences) error {
|
||||||
|
if preferences == nil {
|
||||||
|
return fmt.Errorf("preferences cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate power profile
|
||||||
|
validProfiles := []types.PowerProfile{
|
||||||
|
types.PowerProfileBalanced,
|
||||||
|
types.PowerProfilePerformance,
|
||||||
|
types.PowerProfilePowerSaving,
|
||||||
|
types.PowerProfileCustom,
|
||||||
|
}
|
||||||
|
|
||||||
|
validProfile := false
|
||||||
|
for _, profile := range validProfiles {
|
||||||
|
if preferences.PowerProfile == profile {
|
||||||
|
validProfile = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validProfile {
|
||||||
|
return fmt.Errorf("invalid power profile: %s", preferences.PowerProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate use case
|
||||||
|
validUseCases := []types.UseCase{
|
||||||
|
types.UseCaseGeneral,
|
||||||
|
types.UseCaseDevelopment,
|
||||||
|
types.UseCaseGaming,
|
||||||
|
types.UseCaseServer,
|
||||||
|
types.UseCaseMultimedia,
|
||||||
|
types.UseCaseOffice,
|
||||||
|
}
|
||||||
|
|
||||||
|
validUseCase := false
|
||||||
|
for _, useCase := range validUseCases {
|
||||||
|
if preferences.UseCase == useCase {
|
||||||
|
validUseCase = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validUseCase {
|
||||||
|
return fmt.Errorf("invalid use case: %s", preferences.UseCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for conflicting preferences
|
||||||
|
if preferences.PowerProfile == types.PowerProfilePowerSaving &&
|
||||||
|
preferences.PerformanceMode == types.PerformanceModeMaximum {
|
||||||
|
return fmt.Errorf("conflicting preferences: power saving profile with maximum performance mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if preferences.UseCase == types.UseCaseGaming &&
|
||||||
|
preferences.PowerProfile == types.PowerProfilePowerSaving {
|
||||||
|
fmt.Println("⚠️ Warning: Gaming use case with power saving profile may impact performance")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecommendedSettings provides recommended settings based on preferences
|
||||||
|
func GetRecommendedSettings(preferences *types.UserPreferences) map[string]string {
|
||||||
|
settings := make(map[string]string)
|
||||||
|
|
||||||
|
// Base settings
|
||||||
|
settings["TLP_ENABLE"] = "1"
|
||||||
|
settings["TLP_WARN_LEVEL"] = "3"
|
||||||
|
|
||||||
|
// CPU settings based on power profile and use case
|
||||||
|
switch preferences.PowerProfile {
|
||||||
|
case types.PowerProfilePerformance:
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
|
||||||
|
case types.PowerProfilePowerSaving:
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "powersave"
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||||
|
default: // Balanced
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for use case
|
||||||
|
switch preferences.UseCase {
|
||||||
|
case types.UseCaseGaming:
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||||
|
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
|
||||||
|
settings["PLATFORM_PROFILE_ON_BAT"] = "balanced"
|
||||||
|
case types.UseCaseServer:
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
|
||||||
|
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
|
||||||
|
settings["PLATFORM_PROFILE_ON_BAT"] = "performance"
|
||||||
|
case types.UseCaseDevelopment:
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "ondemand"
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk settings based on battery priority
|
||||||
|
switch preferences.BatteryPriority {
|
||||||
|
case types.BatteryPriorityRuntime:
|
||||||
|
settings["DISK_APM_LEVEL_ON_BAT"] = "1"
|
||||||
|
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "60"
|
||||||
|
case types.BatteryPriorityLongevity:
|
||||||
|
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
|
||||||
|
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "0"
|
||||||
|
default: // Balanced
|
||||||
|
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
|
||||||
|
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "120"
|
||||||
|
}
|
||||||
|
|
||||||
|
settings["DISK_APM_LEVEL_ON_AC"] = "254"
|
||||||
|
settings["DISK_SPINDOWN_TIMEOUT_ON_AC"] = "0"
|
||||||
|
|
||||||
|
// Network settings
|
||||||
|
if utils.Contains(preferences.SpecialRequirements, "Maximum WiFi performance") {
|
||||||
|
settings["WIFI_PWR_ON_AC"] = "off"
|
||||||
|
settings["WIFI_PWR_ON_BAT"] = "off"
|
||||||
|
} else {
|
||||||
|
settings["WIFI_PWR_ON_AC"] = "off"
|
||||||
|
settings["WIFI_PWR_ON_BAT"] = "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
// USB settings
|
||||||
|
settings["USB_AUTOSUSPEND"] = "1"
|
||||||
|
|
||||||
|
// Apply custom settings
|
||||||
|
for key, value := range preferences.CustomSettings {
|
||||||
|
switch key {
|
||||||
|
case "cpu_governor_ac":
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_AC"] = fmt.Sprintf("%v", value)
|
||||||
|
case "cpu_governor_battery":
|
||||||
|
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = fmt.Sprintf("%v", value)
|
||||||
|
case "disk_apm_ac":
|
||||||
|
settings["DISK_APM_LEVEL_ON_AC"] = fmt.Sprintf("%v", value)
|
||||||
|
case "disk_apm_battery":
|
||||||
|
settings["DISK_APM_LEVEL_ON_BAT"] = fmt.Sprintf("%v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings
|
||||||
|
}
|
162
internal/config/preferences_test.go
Normal file
162
internal/config/preferences_test.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidatePreferences(t *testing.T) {
|
||||||
|
validPrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfileBalanced,
|
||||||
|
UseCase: types.UseCaseGeneral,
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeAdaptive,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidatePreferences(validPrefs); err != nil {
|
||||||
|
t.Errorf("ValidatePreferences() with valid preferences failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidatePreferences(nil); err == nil {
|
||||||
|
t.Error("ValidatePreferences() with nil preferences should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidPrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: "invalid_profile",
|
||||||
|
UseCase: types.UseCaseGeneral,
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeAdaptive,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidatePreferences(invalidPrefs); err == nil {
|
||||||
|
t.Error("ValidatePreferences() with invalid power profile should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidPrefs2 := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfileBalanced,
|
||||||
|
UseCase: "invalid_usecase",
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeAdaptive,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidatePreferences(invalidPrefs2); err == nil {
|
||||||
|
t.Error("ValidatePreferences() with invalid use case should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test conflicting preferences
|
||||||
|
conflictingPrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfilePowerSaving,
|
||||||
|
UseCase: types.UseCaseGeneral,
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeMaximum,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidatePreferences(conflictingPrefs); err == nil {
|
||||||
|
t.Error("ValidatePreferences() with conflicting preferences should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecommendedSettings(t *testing.T) {
|
||||||
|
// Test performance profile
|
||||||
|
perfPrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfilePerformance,
|
||||||
|
UseCase: types.UseCaseGaming,
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeMaximum,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := GetRecommendedSettings(perfPrefs)
|
||||||
|
|
||||||
|
// Should have TLP_ENABLE
|
||||||
|
if settings["TLP_ENABLE"] != "1" {
|
||||||
|
t.Error("TLP_ENABLE should be set to '1'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance profile should use performance governor
|
||||||
|
if settings["CPU_SCALING_GOVERNOR_ON_AC"] != "performance" {
|
||||||
|
t.Error("Performance profile should use performance governor on AC")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test power saving profile
|
||||||
|
powerSavePrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfilePowerSaving,
|
||||||
|
UseCase: types.UseCaseOffice,
|
||||||
|
BatteryPriority: types.BatteryPriorityRuntime,
|
||||||
|
PerformanceMode: types.PerformanceModeEfficient,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings2 := GetRecommendedSettings(powerSavePrefs)
|
||||||
|
|
||||||
|
// Power saving should use powersave governor
|
||||||
|
if settings2["CPU_SCALING_GOVERNOR_ON_AC"] != "powersave" {
|
||||||
|
t.Error("Power saving profile should use powersave governor on AC")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings2["CPU_SCALING_GOVERNOR_ON_BAT"] != "powersave" {
|
||||||
|
t.Error("Power saving profile should use powersave governor on battery")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test custom settings
|
||||||
|
customPrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfileCustom,
|
||||||
|
UseCase: types.UseCaseGeneral,
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeAdaptive,
|
||||||
|
CustomSettings: map[string]interface{}{
|
||||||
|
"cpu_governor_ac": "ondemand",
|
||||||
|
"cpu_governor_battery": "conservative",
|
||||||
|
"disk_apm_ac": "200",
|
||||||
|
"disk_apm_battery": "100",
|
||||||
|
},
|
||||||
|
SpecialRequirements: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings3 := GetRecommendedSettings(customPrefs)
|
||||||
|
|
||||||
|
// Custom settings should override defaults
|
||||||
|
if settings3["CPU_SCALING_GOVERNOR_ON_AC"] != "ondemand" {
|
||||||
|
t.Error("Custom CPU governor AC setting should be applied")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings3["CPU_SCALING_GOVERNOR_ON_BAT"] != "conservative" {
|
||||||
|
t.Error("Custom CPU governor battery setting should be applied")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings3["DISK_APM_LEVEL_ON_AC"] != "200" {
|
||||||
|
t.Error("Custom disk APM AC setting should be applied")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings3["DISK_APM_LEVEL_ON_BAT"] != "100" {
|
||||||
|
t.Error("Custom disk APM battery setting should be applied")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test special requirements
|
||||||
|
wifiPrefs := &types.UserPreferences{
|
||||||
|
PowerProfile: types.PowerProfileBalanced,
|
||||||
|
UseCase: types.UseCaseGeneral,
|
||||||
|
BatteryPriority: types.BatteryPriorityBalanced,
|
||||||
|
PerformanceMode: types.PerformanceModeAdaptive,
|
||||||
|
CustomSettings: make(map[string]interface{}),
|
||||||
|
SpecialRequirements: []string{"Maximum WiFi performance"},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings4 := GetRecommendedSettings(wifiPrefs)
|
||||||
|
|
||||||
|
// WiFi performance requirement should disable power saving
|
||||||
|
if settings4["WIFI_PWR_ON_AC"] != "off" || settings4["WIFI_PWR_ON_BAT"] != "off" {
|
||||||
|
t.Error("Maximum WiFi performance should disable WiFi power saving")
|
||||||
|
}
|
||||||
|
}
|
267
internal/security/keystore.go
Normal file
267
internal/security/keystore.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyStore handles secure storage and retrieval of API keys
|
||||||
|
type KeyStore struct {
|
||||||
|
configDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyStore creates a new KeyStore instance
|
||||||
|
func NewKeyStore() (*KeyStore, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Join(homeDir, ".config", "autotlp")
|
||||||
|
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KeyStore{
|
||||||
|
configDir: configDir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreAPIKey securely stores an API key
|
||||||
|
func (ks *KeyStore) StoreAPIKey(provider, apiKey string) error {
|
||||||
|
if apiKey == "" {
|
||||||
|
return fmt.Errorf("API key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create master password
|
||||||
|
masterPassword, err := ks.getMasterPassword()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get master password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the API key
|
||||||
|
encryptedKey, err := ks.encrypt(apiKey, masterPassword)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt API key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store encrypted key
|
||||||
|
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||||
|
if err := os.WriteFile(keyFile, []byte(encryptedKey), 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrieveAPIKey retrieves and decrypts an API key
|
||||||
|
func (ks *KeyStore) RetrieveAPIKey(provider string) (string, error) {
|
||||||
|
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||||
|
|
||||||
|
// Check if key file exists
|
||||||
|
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("API key not found for provider: %s", provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read encrypted key
|
||||||
|
encryptedKey, err := os.ReadFile(keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get master password
|
||||||
|
masterPassword, err := ks.getMasterPassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get master password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the API key
|
||||||
|
apiKey, err := ks.decrypt(string(encryptedKey), masterPassword)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decrypt API key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAPIKey removes a stored API key
|
||||||
|
func (ks *KeyStore) DeleteAPIKey(provider string) error {
|
||||||
|
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||||
|
|
||||||
|
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to delete key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProviders returns a list of providers with stored keys
|
||||||
|
func (ks *KeyStore) ListProviders() ([]string, error) {
|
||||||
|
files, err := os.ReadDir(ks.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var providers []string
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() && filepath.Ext(file.Name()) == ".key" {
|
||||||
|
provider := file.Name()[:len(file.Name())-4] // Remove .key extension
|
||||||
|
providers = append(providers, provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return providers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMasterPassword gets or creates a master password for encryption
|
||||||
|
func (ks *KeyStore) getMasterPassword() (string, error) {
|
||||||
|
passwordFile := filepath.Join(ks.configDir, ".master")
|
||||||
|
|
||||||
|
// Check if master password file exists
|
||||||
|
if _, err := os.Stat(passwordFile); os.IsNotExist(err) {
|
||||||
|
// Create new master password
|
||||||
|
fmt.Print("Create a master password to secure your API keys: ")
|
||||||
|
password, err := ks.readPassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Confirm master password: ")
|
||||||
|
confirmPassword, err := ks.readPassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read confirmation password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if password != confirmPassword {
|
||||||
|
return "", fmt.Errorf("passwords do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash and store the password
|
||||||
|
hashedPassword := ks.hashPassword(password)
|
||||||
|
if err := os.WriteFile(passwordFile, []byte(hashedPassword), 0600); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to store master password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master password exists, prompt for it
|
||||||
|
fmt.Print("Enter master password: ")
|
||||||
|
password, err := ks.readPassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
storedHash, err := os.ReadFile(passwordFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read stored password hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ks.hashPassword(password) != string(storedHash) {
|
||||||
|
return "", fmt.Errorf("incorrect master password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPassword reads a password from stdin without echoing
|
||||||
|
func (ks *KeyStore) readPassword() (string, error) {
|
||||||
|
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fmt.Println() // Add newline after password input
|
||||||
|
return string(password), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashPassword creates a hash of the password for verification
|
||||||
|
func (ks *KeyStore) hashPassword(password string) string {
|
||||||
|
hash := sha256.Sum256([]byte(password))
|
||||||
|
return base64.StdEncoding.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt encrypts data using AES-GCM
|
||||||
|
func (ks *KeyStore) encrypt(plaintext, password string) (string, error) {
|
||||||
|
// Create cipher
|
||||||
|
key := sha256.Sum256([]byte(password))
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GCM
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate nonce
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt decrypts data using AES-GCM
|
||||||
|
func (ks *KeyStore) decrypt(ciphertext, password string) (string, error) {
|
||||||
|
// Decode base64
|
||||||
|
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cipher
|
||||||
|
key := sha256.Sum256([]byte(password))
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GCM
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract nonce
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(data) < nonceSize {
|
||||||
|
return "", fmt.Errorf("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureInput prompts for secure input without echoing
|
||||||
|
func SecureInput(prompt string) (string, error) {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
input, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fmt.Println() // Add newline
|
||||||
|
return string(input), nil
|
||||||
|
}
|
249
internal/security/privileges.go
Normal file
249
internal/security/privileges.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrivilegeManager handles privilege escalation and security operations
|
||||||
|
type PrivilegeManager struct {
|
||||||
|
logger *utils.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivilegeManager creates a new privilege manager
|
||||||
|
func NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager {
|
||||||
|
return &PrivilegeManager{
|
||||||
|
logger: logger.WithComponent("security"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireRoot ensures the current operation has root privileges
|
||||||
|
func (pm *PrivilegeManager) RequireRoot(operation string) error {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.logger.Info("Root privileges required", "operation", operation)
|
||||||
|
return fmt.Errorf("operation '%s' requires root privileges", operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscalateIfNeeded escalates privileges if not already running as root
|
||||||
|
func (pm *PrivilegeManager) EscalateIfNeeded(args []string, operation string) error {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.logger.Info("Escalating privileges", "operation", operation)
|
||||||
|
|
||||||
|
// Check if sudo is available
|
||||||
|
if !utils.CommandExists("sudo") {
|
||||||
|
return fmt.Errorf("sudo is required for privilege escalation but not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform user about privilege escalation
|
||||||
|
fmt.Printf("\nPrivilege escalation required for: %s\n", operation)
|
||||||
|
fmt.Println("This operation needs administrative privileges to:")
|
||||||
|
|
||||||
|
switch operation {
|
||||||
|
case "install_tlp":
|
||||||
|
fmt.Println("- Install TLP packages using system package manager")
|
||||||
|
fmt.Println("- Enable and start TLP systemd service")
|
||||||
|
fmt.Println("- Mask conflicting power management services")
|
||||||
|
case "apply_config":
|
||||||
|
fmt.Println("- Write TLP configuration to system directories")
|
||||||
|
fmt.Println("- Reload TLP service with new configuration")
|
||||||
|
case "system_info":
|
||||||
|
fmt.Println("- Access hardware information from system files")
|
||||||
|
fmt.Println("- Read power management settings")
|
||||||
|
default:
|
||||||
|
fmt.Printf("- Perform system operation: %s\n", operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.GetUserConfirmation("Continue with privilege escalation?") {
|
||||||
|
return fmt.Errorf("privilege escalation declined by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute with sudo
|
||||||
|
return pm.executeSudo(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeSudo executes the current program with sudo
|
||||||
|
func (pm *PrivilegeManager) executeSudo(args []string) error {
|
||||||
|
// Get current executable path
|
||||||
|
executable, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare sudo command
|
||||||
|
sudoArgs := append([]string{executable}, args...)
|
||||||
|
cmd := exec.Command("sudo", sudoArgs...)
|
||||||
|
|
||||||
|
// Preserve environment variables that might be needed
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
|
||||||
|
// Connect stdio
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
pm.logger.Info("Executing with sudo", "command", cmd.String())
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
// Get exit code from the elevated process
|
||||||
|
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||||
|
os.Exit(status.ExitStatus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("sudo execution failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here, the elevated process completed successfully
|
||||||
|
os.Exit(0)
|
||||||
|
return nil // This line will never be reached
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckSudoAccess verifies that the user can use sudo
|
||||||
|
func (pm *PrivilegeManager) CheckSudoAccess() error {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
return nil // Already root
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.CommandExists("sudo") {
|
||||||
|
return fmt.Errorf("sudo command not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test sudo access with a harmless command
|
||||||
|
cmd := exec.Command("sudo", "-n", "true")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
// -n flag failed, user needs to authenticate
|
||||||
|
pm.logger.Debug("Sudo authentication required")
|
||||||
|
return nil // This is expected for most users
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.logger.Debug("Sudo access confirmed without authentication")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropPrivileges drops root privileges if running as root
|
||||||
|
func (pm *PrivilegeManager) DropPrivileges() error {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
return nil // Not running as root
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the original user info from environment
|
||||||
|
sudoUID := os.Getenv("SUDO_UID")
|
||||||
|
sudoGID := os.Getenv("SUDO_GID")
|
||||||
|
|
||||||
|
if sudoUID == "" || sudoGID == "" {
|
||||||
|
pm.logger.Warn("Cannot drop privileges: SUDO_UID/SUDO_GID not set")
|
||||||
|
return nil // Don't fail, just log warning
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.logger.Info("Dropping root privileges", "uid", sudoUID, "gid", sudoGID)
|
||||||
|
|
||||||
|
// This is a placeholder - actual privilege dropping requires careful handling
|
||||||
|
// and is typically done at the start of the program, not mid-execution
|
||||||
|
pm.logger.Debug("Privilege dropping not implemented for mid-execution")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSystemAccess validates that the program has necessary system access
|
||||||
|
func (pm *PrivilegeManager) ValidateSystemAccess() error {
|
||||||
|
// Check read access to system information
|
||||||
|
systemPaths := []string{
|
||||||
|
"/proc/cpuinfo",
|
||||||
|
"/proc/meminfo",
|
||||||
|
"/sys/class/power_supply",
|
||||||
|
"/sys/devices/system/cpu",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range systemPaths {
|
||||||
|
if utils.FileExists(path) {
|
||||||
|
// Try to read the file/directory
|
||||||
|
if file, err := os.Open(path); err != nil {
|
||||||
|
pm.logger.Warn("Limited access to system path", "path", path, "error", err)
|
||||||
|
} else {
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureFilePermissions sets secure permissions on configuration files
|
||||||
|
func (pm *PrivilegeManager) SecureFilePermissions(filePath string) error {
|
||||||
|
// Set file permissions to be readable only by owner and group
|
||||||
|
if err := os.Chmod(filePath, 0640); err != nil {
|
||||||
|
return fmt.Errorf("failed to set secure permissions on %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.logger.Debug("Set secure file permissions", "file", filePath, "mode", "0640")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateInput performs basic input validation for security
|
||||||
|
func ValidateInput(input string, maxLength int, allowedChars string) error {
|
||||||
|
if len(input) > maxLength {
|
||||||
|
return fmt.Errorf("input too long: %d characters (max %d)", len(input), maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) == 0 {
|
||||||
|
return fmt.Errorf("input cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation against null bytes and control characters
|
||||||
|
for i, r := range input {
|
||||||
|
if r == 0 {
|
||||||
|
return fmt.Errorf("null byte at position %d", i)
|
||||||
|
}
|
||||||
|
if r < 32 && r != 9 && r != 10 && r != 13 { // Allow tab, LF, CR
|
||||||
|
return fmt.Errorf("control character at position %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeFilePath sanitizes file paths to prevent directory traversal
|
||||||
|
func SanitizeFilePath(path string) (string, error) {
|
||||||
|
// Basic path validation
|
||||||
|
if path == "" {
|
||||||
|
return "", fmt.Errorf("empty path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for directory traversal attempts
|
||||||
|
if len(path) > 1 && (path[0] == '/' || path[1] == ':') {
|
||||||
|
// Absolute path - this might be intentional, so we allow it
|
||||||
|
// but log it for security awareness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous patterns
|
||||||
|
dangerousPatterns := []string{
|
||||||
|
"../",
|
||||||
|
"..\\",
|
||||||
|
"./",
|
||||||
|
".\\",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range dangerousPatterns {
|
||||||
|
if len(path) >= len(pattern) {
|
||||||
|
for i := 0; i <= len(path)-len(pattern); i++ {
|
||||||
|
if path[i:i+len(pattern)] == pattern {
|
||||||
|
return "", fmt.Errorf("potentially dangerous path pattern: %s", pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
487
internal/system/detector.go
Normal file
487
internal/system/detector.go
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
Distribution string
|
||||||
|
Version string
|
||||||
|
Architecture string
|
||||||
|
PackageManager string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectSystem(ctx context.Context) (*Info, error) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &Info{
|
||||||
|
Architecture: runtime.GOARCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := detectFromOSRelease(info); err != nil {
|
||||||
|
if err := detectFromLSBRelease(info); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to detect Linux distribution: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.PackageManager = detectPackageManager(info.Distribution)
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectFromOSRelease(info *Info) error {
|
||||||
|
const osReleasePath = "/etc/os-release"
|
||||||
|
|
||||||
|
if !utils.FileExists(osReleasePath) {
|
||||||
|
return fmt.Errorf("os-release file not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, err := utils.ReadFileLines(osReleasePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read os-release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
osInfo := make(map[string]string)
|
||||||
|
for _, line := range lines {
|
||||||
|
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||||
|
osInfo[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, exists := osInfo["ID"]; exists {
|
||||||
|
info.Distribution = id
|
||||||
|
}
|
||||||
|
|
||||||
|
if version, exists := osInfo["VERSION_ID"]; exists {
|
||||||
|
info.Version = version
|
||||||
|
} else if version, exists := osInfo["VERSION"]; exists {
|
||||||
|
info.Version = version
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Distribution == "" {
|
||||||
|
return fmt.Errorf("could not determine distribution from os-release")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectFromLSBRelease(info *Info) error {
|
||||||
|
if !utils.CommandExists("lsb_release") {
|
||||||
|
return fmt.Errorf("lsb_release command not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
distro, err := utils.RunCommand("lsb_release", "-si")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get distribution ID: %w", err)
|
||||||
|
}
|
||||||
|
info.Distribution = strings.ToLower(distro)
|
||||||
|
|
||||||
|
version, err := utils.RunCommand("lsb_release", "-sr")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get distribution version: %w", err)
|
||||||
|
}
|
||||||
|
info.Version = version
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPackageManager(distro string) string {
|
||||||
|
switch strings.ToLower(distro) {
|
||||||
|
case "ubuntu", "debian", "linuxmint", "elementary", "pop":
|
||||||
|
return "apt"
|
||||||
|
case "fedora", "rhel", "centos", "rocky", "almalinux":
|
||||||
|
return "dnf"
|
||||||
|
case "opensuse", "suse", "opensuse-leap", "opensuse-tumbleweed":
|
||||||
|
return "zypper"
|
||||||
|
case "arch", "manjaro", "endeavouros", "garuda":
|
||||||
|
return "pacman"
|
||||||
|
case "gentoo":
|
||||||
|
return "portage"
|
||||||
|
case "alpine":
|
||||||
|
return "apk"
|
||||||
|
default:
|
||||||
|
if utils.CommandExists("apt") {
|
||||||
|
return "apt"
|
||||||
|
} else if utils.CommandExists("dnf") {
|
||||||
|
return "dnf"
|
||||||
|
} else if utils.CommandExists("yum") {
|
||||||
|
return "yum"
|
||||||
|
} else if utils.CommandExists("zypper") {
|
||||||
|
return "zypper"
|
||||||
|
} else if utils.CommandExists("pacman") {
|
||||||
|
return "pacman"
|
||||||
|
} else if utils.CommandExists("apk") {
|
||||||
|
return "apk"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GatherSystemInfo(ctx context.Context) (*types.SystemInfo, error) {
|
||||||
|
sysInfo := &types.SystemInfo{}
|
||||||
|
|
||||||
|
basicInfo, err := DetectSystem(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to detect system: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sysInfo.Distribution = types.DistributionInfo{
|
||||||
|
ID: basicInfo.Distribution,
|
||||||
|
Version: basicInfo.Version,
|
||||||
|
PackageManager: basicInfo.PackageManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cpuInfo, err := gatherCPUInfo(); err == nil {
|
||||||
|
sysInfo.CPU = *cpuInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if memInfo, err := gatherMemoryInfo(); err == nil {
|
||||||
|
sysInfo.Memory = *memInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if batteryInfo, err := gatherBatteryInfo(); err == nil {
|
||||||
|
sysInfo.Battery = batteryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if powerInfo, err := gatherPowerSupplyInfo(); err == nil {
|
||||||
|
sysInfo.PowerSupply = *powerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if kernelInfo, err := gatherKernelInfo(); err == nil {
|
||||||
|
sysInfo.Kernel = *kernelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if hwInfo, err := gatherHardwareInfo(); err == nil {
|
||||||
|
sysInfo.Hardware = *hwInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return sysInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherCPUInfo() (*types.CPUInfo, error) {
|
||||||
|
cpuInfo := &types.CPUInfo{}
|
||||||
|
|
||||||
|
if utils.FileExists("/proc/cpuinfo") {
|
||||||
|
lines, err := utils.ReadFileLines("/proc/cpuinfo")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "model name":
|
||||||
|
if cpuInfo.Model == "" {
|
||||||
|
cpuInfo.Model = value
|
||||||
|
}
|
||||||
|
case "vendor_id":
|
||||||
|
if cpuInfo.Vendor == "" {
|
||||||
|
cpuInfo.Vendor = value
|
||||||
|
}
|
||||||
|
case "processor":
|
||||||
|
cpuInfo.Cores++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") {
|
||||||
|
governor, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
|
||||||
|
if err == nil {
|
||||||
|
cpuInfo.Governor = governor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") {
|
||||||
|
maxFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
|
||||||
|
if err == nil {
|
||||||
|
cpuInfo.MaxFrequency = utils.ParseInt64(maxFreq) / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq") {
|
||||||
|
minFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq")
|
||||||
|
if err == nil {
|
||||||
|
cpuInfo.MinFrequency = utils.ParseInt64(minFreq) / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuInfo.Architecture = runtime.GOARCH
|
||||||
|
|
||||||
|
return cpuInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherMemoryInfo() (*types.MemoryInfo, error) {
|
||||||
|
memInfo := &types.MemoryInfo{}
|
||||||
|
|
||||||
|
if !utils.FileExists("/proc/meminfo") {
|
||||||
|
return nil, fmt.Errorf("/proc/meminfo not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, err := utils.ReadFileLines("/proc/meminfo")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSuffix(parts[0], ":")
|
||||||
|
value := utils.ParseInt64(parts[1])
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "MemTotal":
|
||||||
|
memInfo.Total = value / 1024
|
||||||
|
case "MemAvailable":
|
||||||
|
memInfo.Available = value / 1024
|
||||||
|
case "SwapTotal":
|
||||||
|
memInfo.SwapTotal = value / 1024
|
||||||
|
case "SwapFree":
|
||||||
|
swapFree := value / 1024
|
||||||
|
memInfo.SwapUsed = memInfo.SwapTotal - swapFree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memInfo.Used = memInfo.Total - memInfo.Available
|
||||||
|
|
||||||
|
return memInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherBatteryInfo() (*types.BatteryInfo, error) {
|
||||||
|
batteryPath := "/sys/class/power_supply/BAT0"
|
||||||
|
if !utils.FileExists(batteryPath) {
|
||||||
|
batteryPath = "/sys/class/power_supply/BAT1"
|
||||||
|
if !utils.FileExists(batteryPath) {
|
||||||
|
return nil, fmt.Errorf("no battery found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batteryInfo := &types.BatteryInfo{Present: true}
|
||||||
|
|
||||||
|
properties := map[string]*string{
|
||||||
|
"status": &batteryInfo.Status,
|
||||||
|
"manufacturer": &batteryInfo.Manufacturer,
|
||||||
|
"model_name": &batteryInfo.Model,
|
||||||
|
"technology": &batteryInfo.Technology,
|
||||||
|
}
|
||||||
|
|
||||||
|
for prop, field := range properties {
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/" + prop); err == nil {
|
||||||
|
*field = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/capacity"); err == nil {
|
||||||
|
batteryInfo.Capacity = utils.ParseInt(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full"); err == nil {
|
||||||
|
batteryInfo.EnergyFull = utils.ParseInt64(value) / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/energy_now"); err == nil {
|
||||||
|
batteryInfo.EnergyNow = utils.ParseInt64(value) / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/power_now"); err == nil {
|
||||||
|
batteryInfo.PowerNow = utils.ParseInt64(value) / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/cycle_count"); err == nil {
|
||||||
|
batteryInfo.CycleCount = utils.ParseInt(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full_design"); err == nil {
|
||||||
|
batteryInfo.DesignCapacity = utils.ParseInt64(value) / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
return batteryInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherPowerSupplyInfo() (*types.PowerSupplyInfo, error) {
|
||||||
|
powerInfo := &types.PowerSupplyInfo{}
|
||||||
|
|
||||||
|
acPaths := []string{
|
||||||
|
"/sys/class/power_supply/ADP0",
|
||||||
|
"/sys/class/power_supply/ADP1",
|
||||||
|
"/sys/class/power_supply/AC",
|
||||||
|
"/sys/class/power_supply/ACAD",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range acPaths {
|
||||||
|
if utils.FileExists(path + "/online") {
|
||||||
|
if online, err := utils.ReadFirstLine(path + "/online"); err == nil {
|
||||||
|
powerInfo.ACConnected = online == "1"
|
||||||
|
powerInfo.Online = powerInfo.ACConnected
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
powerInfo.Type = "AC"
|
||||||
|
|
||||||
|
return powerInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherKernelInfo() (*types.KernelInfo, error) {
|
||||||
|
kernelInfo := &types.KernelInfo{
|
||||||
|
Parameters: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
if version, err := utils.ReadFirstLine("/proc/version"); err == nil {
|
||||||
|
parts := strings.Fields(version)
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
kernelInfo.Version = parts[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if release, err := utils.ReadFirstLine("/proc/sys/kernel/osrelease"); err == nil {
|
||||||
|
kernelInfo.Release = release
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmdline, err := utils.ReadFirstLine("/proc/cmdline"); err == nil {
|
||||||
|
params := strings.Fields(cmdline)
|
||||||
|
for _, param := range params {
|
||||||
|
if key, value, ok := utils.ParseKeyValue(param); ok {
|
||||||
|
kernelInfo.Parameters[key] = value
|
||||||
|
} else {
|
||||||
|
kernelInfo.Parameters[param] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return kernelInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherHardwareInfo() (*types.HardwareInfo, error) {
|
||||||
|
hwInfo := &types.HardwareInfo{}
|
||||||
|
|
||||||
|
if utils.FileExists("/sys/class/dmi/id/chassis_type") {
|
||||||
|
if chassis, err := utils.ReadFirstLine("/sys/class/dmi/id/chassis_type"); err == nil {
|
||||||
|
hwInfo.Chassis = getChassisType(utils.ParseInt(chassis))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.FileExists("/sys/class/dmi/id/sys_vendor") {
|
||||||
|
if vendor, err := utils.ReadFirstLine("/sys/class/dmi/id/sys_vendor"); err == nil {
|
||||||
|
hwInfo.Manufacturer = vendor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.FileExists("/sys/class/dmi/id/product_name") {
|
||||||
|
if product, err := utils.ReadFirstLine("/sys/class/dmi/id/product_name"); err == nil {
|
||||||
|
hwInfo.ProductName = product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hwInfo.StorageDevices = gatherStorageInfo()
|
||||||
|
|
||||||
|
return hwInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChassisType(chassisType int) string {
|
||||||
|
types := map[int]string{
|
||||||
|
1: "Other",
|
||||||
|
2: "Unknown",
|
||||||
|
3: "Desktop",
|
||||||
|
4: "Low Profile Desktop",
|
||||||
|
5: "Pizza Box",
|
||||||
|
6: "Mini Tower",
|
||||||
|
7: "Tower",
|
||||||
|
8: "Portable",
|
||||||
|
9: "Laptop",
|
||||||
|
10: "Notebook",
|
||||||
|
11: "Hand Held",
|
||||||
|
12: "Docking Station",
|
||||||
|
13: "All In One",
|
||||||
|
14: "Sub Notebook",
|
||||||
|
15: "Space-saving",
|
||||||
|
16: "Lunch Box",
|
||||||
|
17: "Main Server Chassis",
|
||||||
|
18: "Expansion Chassis",
|
||||||
|
19: "Sub Chassis",
|
||||||
|
20: "Bus Expansion Chassis",
|
||||||
|
21: "Peripheral Chassis",
|
||||||
|
22: "RAID Chassis",
|
||||||
|
23: "Rack Mount Chassis",
|
||||||
|
24: "Sealed-case PC",
|
||||||
|
25: "Multi-system",
|
||||||
|
26: "CompactPCI",
|
||||||
|
27: "AdvancedTCA",
|
||||||
|
28: "Blade",
|
||||||
|
29: "Blade Enclosing",
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc, exists := types[chassisType]; exists {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherStorageInfo() []types.StorageInfo {
|
||||||
|
var devices []types.StorageInfo
|
||||||
|
|
||||||
|
if !utils.FileExists("/proc/partitions") {
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, err := utils.ReadFileLines("/proc/partitions")
|
||||||
|
if err != nil {
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceName := fields[3]
|
||||||
|
if strings.HasPrefix(line, "major") ||
|
||||||
|
strings.Contains(deviceName, "loop") ||
|
||||||
|
len(deviceName) > 3 && (deviceName[len(deviceName)-1] >= '0' && deviceName[len(deviceName)-1] <= '9') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
device := types.StorageInfo{
|
||||||
|
Device: "/dev/" + deviceName,
|
||||||
|
Size: utils.ParseInt64(fields[2]) / 1024 / 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
rotationalPath := fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)
|
||||||
|
if utils.FileExists(rotationalPath) {
|
||||||
|
if rotational, err := utils.ReadFirstLine(rotationalPath); err == nil {
|
||||||
|
device.Rotational = rotational == "1"
|
||||||
|
if device.Rotational {
|
||||||
|
device.Type = "HDD"
|
||||||
|
} else if strings.HasPrefix(deviceName, "nvme") {
|
||||||
|
device.Type = "NVMe"
|
||||||
|
} else {
|
||||||
|
device.Type = "SSD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelPath := fmt.Sprintf("/sys/block/%s/device/model", deviceName)
|
||||||
|
if utils.FileExists(modelPath) {
|
||||||
|
if model, err := utils.ReadFirstLine(modelPath); err == nil {
|
||||||
|
device.Model = strings.TrimSpace(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devices = append(devices, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices
|
||||||
|
}
|
121
internal/system/detector_test.go
Normal file
121
internal/system/detector_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectPackageManager(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
distro string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"ubuntu", "apt"},
|
||||||
|
{"debian", "apt"},
|
||||||
|
{"fedora", "dnf"},
|
||||||
|
{"centos", "dnf"},
|
||||||
|
{"arch", "pacman"},
|
||||||
|
{"manjaro", "pacman"},
|
||||||
|
{"opensuse", "zypper"},
|
||||||
|
{"alpine", "apk"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := detectPackageManager(test.distro)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("detectPackageManager(%q) = %q, want %q", test.distro, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unknownResult := detectPackageManager("totally_unknown_distro")
|
||||||
|
if unknownResult == "" {
|
||||||
|
t.Error("detectPackageManager with unknown distro should return something")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectSystem(t *testing.T) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
info, err := DetectSystem(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DetectSystem() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Distribution == "" {
|
||||||
|
t.Error("Distribution should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Architecture == "" {
|
||||||
|
t.Error("Architecture should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.PackageManager == "" {
|
||||||
|
t.Error("PackageManager should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Architecture should match runtime.GOARCH
|
||||||
|
if info.Architecture != runtime.GOARCH {
|
||||||
|
t.Errorf("Architecture = %q, want %q", info.Architecture, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetChassisType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{1, "Other"},
|
||||||
|
{3, "Desktop"},
|
||||||
|
{9, "Laptop"},
|
||||||
|
{10, "Notebook"},
|
||||||
|
{999, "Unknown"}, // Invalid type
|
||||||
|
{0, "Unknown"}, // Invalid type
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := getChassisType(test.input)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("getChassisType(%d) = %q, want %q", test.input, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatherSystemInfo(t *testing.T) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sysInfo, err := GatherSystemInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GatherSystemInfo() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if sysInfo.Distribution.ID == "" {
|
||||||
|
t.Error("Distribution ID should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysInfo.CPU.Architecture == "" {
|
||||||
|
t.Error("CPU Architecture should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysInfo.Memory.Total <= 0 {
|
||||||
|
t.Error("Memory Total should be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU should have at least 1 core (but may be 0 if /proc/cpuinfo is not accessible in test environment)
|
||||||
|
if sysInfo.CPU.Cores < 0 {
|
||||||
|
t.Error("CPU Cores should not be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can read CPU info, we should have at least 1 core
|
||||||
|
// In test environments, this might not be available, so we just log it
|
||||||
|
if sysInfo.CPU.Cores == 0 {
|
||||||
|
t.Logf("Warning: CPU cores is 0, possibly due to test environment limitations")
|
||||||
|
}
|
||||||
|
}
|
358
internal/tlp/manager.go
Normal file
358
internal/tlp/manager.go
Normal 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
|
||||||
|
}
|
210
pkg/types/types.go
Normal file
210
pkg/types/types.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SystemInfo represents comprehensive system information
|
||||||
|
type SystemInfo struct {
|
||||||
|
CPU CPUInfo `json:"cpu"`
|
||||||
|
Memory MemoryInfo `json:"memory"`
|
||||||
|
Battery *BatteryInfo `json:"battery,omitempty"`
|
||||||
|
PowerSupply PowerSupplyInfo `json:"power_supply"`
|
||||||
|
Kernel KernelInfo `json:"kernel"`
|
||||||
|
Distribution DistributionInfo `json:"distribution"`
|
||||||
|
Hardware HardwareInfo `json:"hardware"`
|
||||||
|
TLPStatus TLPStatus `json:"tlp_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPUInfo contains CPU-related information
|
||||||
|
type CPUInfo struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Vendor string `json:"vendor"`
|
||||||
|
Cores int `json:"cores"`
|
||||||
|
Threads int `json:"threads"`
|
||||||
|
BaseFrequency int64 `json:"base_frequency_mhz"`
|
||||||
|
MaxFrequency int64 `json:"max_frequency_mhz"`
|
||||||
|
MinFrequency int64 `json:"min_frequency_mhz"`
|
||||||
|
Governor string `json:"governor"`
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryInfo contains memory information
|
||||||
|
type MemoryInfo struct {
|
||||||
|
Total int64 `json:"total_mb"`
|
||||||
|
Available int64 `json:"available_mb"`
|
||||||
|
Used int64 `json:"used_mb"`
|
||||||
|
SwapTotal int64 `json:"swap_total_mb"`
|
||||||
|
SwapUsed int64 `json:"swap_used_mb"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatteryInfo contains battery information
|
||||||
|
type BatteryInfo struct {
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Capacity int `json:"capacity_percent"`
|
||||||
|
EnergyFull int64 `json:"energy_full_wh"`
|
||||||
|
EnergyNow int64 `json:"energy_now_wh"`
|
||||||
|
PowerNow int64 `json:"power_now_w"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Technology string `json:"technology"`
|
||||||
|
CycleCount int `json:"cycle_count"`
|
||||||
|
DesignCapacity int64 `json:"design_capacity_wh"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerSupplyInfo contains power supply information
|
||||||
|
type PowerSupplyInfo struct {
|
||||||
|
ACConnected bool `json:"ac_connected"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KernelInfo contains kernel information
|
||||||
|
type KernelInfo struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Release string `json:"release"`
|
||||||
|
Parameters map[string]string `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistributionInfo contains Linux distribution information
|
||||||
|
type DistributionInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Codename string `json:"codename"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
PackageManager string `json:"package_manager"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HardwareInfo contains additional hardware information
|
||||||
|
type HardwareInfo struct {
|
||||||
|
Chassis string `json:"chassis"`
|
||||||
|
Manufacturer string `json:"manufacturer"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
GPUs []GPUInfo `json:"gpus"`
|
||||||
|
NetworkCards []string `json:"network_cards"`
|
||||||
|
StorageDevices []StorageInfo `json:"storage_devices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPUInfo contains GPU information
|
||||||
|
type GPUInfo struct {
|
||||||
|
Vendor string `json:"vendor"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Driver string `json:"driver"`
|
||||||
|
Memory int64 `json:"memory_mb"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageInfo contains storage device information
|
||||||
|
type StorageInfo struct {
|
||||||
|
Device string `json:"device"`
|
||||||
|
Type string `json:"type"` // SSD, HDD, NVMe
|
||||||
|
Size int64 `json:"size_gb"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Rotational bool `json:"rotational"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLPStatus contains TLP installation and configuration status
|
||||||
|
type TLPStatus struct {
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
ConfigPath string `json:"config_path"`
|
||||||
|
ConfigExists bool `json:"config_exists"`
|
||||||
|
CurrentConfig map[string]string `json:"current_config"`
|
||||||
|
LastModified *time.Time `json:"last_modified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPreferences represents user's power management preferences
|
||||||
|
type UserPreferences struct {
|
||||||
|
PowerProfile PowerProfile `json:"power_profile"`
|
||||||
|
UseCase UseCase `json:"use_case"`
|
||||||
|
BatteryPriority BatteryPriority `json:"battery_priority"`
|
||||||
|
PerformanceMode PerformanceMode `json:"performance_mode"`
|
||||||
|
CustomSettings map[string]interface{} `json:"custom_settings"`
|
||||||
|
SpecialRequirements []string `json:"special_requirements"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerProfile represents different power usage profiles
|
||||||
|
type PowerProfile string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PowerProfileBalanced PowerProfile = "balanced"
|
||||||
|
PowerProfilePerformance PowerProfile = "performance"
|
||||||
|
PowerProfilePowerSaving PowerProfile = "power_saving"
|
||||||
|
PowerProfileCustom PowerProfile = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UseCase represents different system use cases
|
||||||
|
type UseCase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UseCaseGeneral UseCase = "general"
|
||||||
|
UseCaseDevelopment UseCase = "development"
|
||||||
|
UseCaseGaming UseCase = "gaming"
|
||||||
|
UseCaseServer UseCase = "server"
|
||||||
|
UseCaseMultimedia UseCase = "multimedia"
|
||||||
|
UseCaseOffice UseCase = "office"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatteryPriority represents battery optimization priority
|
||||||
|
type BatteryPriority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BatteryPriorityLongevity BatteryPriority = "longevity"
|
||||||
|
BatteryPriorityRuntime BatteryPriority = "runtime"
|
||||||
|
BatteryPriorityBalanced BatteryPriority = "balanced"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PerformanceMode represents performance optimization mode
|
||||||
|
type PerformanceMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PerformanceModeMaximum PerformanceMode = "maximum"
|
||||||
|
PerformanceModeAdaptive PerformanceMode = "adaptive"
|
||||||
|
PerformanceModeEfficient PerformanceMode = "efficient"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AIProvider represents different AI service providers
|
||||||
|
type AIProvider string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AIProviderOpenRouter AIProvider = "openrouter"
|
||||||
|
AIProviderGroq AIProvider = "groq"
|
||||||
|
AIProviderGemini AIProvider = "gemini"
|
||||||
|
AIProviderCustom AIProvider = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AIConfig represents AI service configuration
|
||||||
|
type AIConfig struct {
|
||||||
|
Provider AIProvider `json:"provider"`
|
||||||
|
APIKey string `json:"-"` // Never serialize API keys
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLPConfiguration represents a complete TLP configuration
|
||||||
|
type TLPConfiguration struct {
|
||||||
|
Settings map[string]string `json:"settings"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Rationale map[string]string `json:"rationale"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
Generated time.Time `json:"generated"`
|
||||||
|
SystemInfo *SystemInfo `json:"system_info"`
|
||||||
|
Preferences *UserPreferences `json:"preferences"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallationResult represents the result of TLP installation
|
||||||
|
type InstallationResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ConfigPath string `json:"config_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationResult represents configuration validation result
|
||||||
|
type ValidationResult struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
}
|
233
pkg/utils/helpers.go
Normal file
233
pkg/utils/helpers.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileExists checks if a file exists
|
||||||
|
func FileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFileLines reads a file and returns its lines
|
||||||
|
func ReadFileLines(path string) ([]string, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFirstLine reads the first line of a file
|
||||||
|
func ReadFirstLine(path string) (string, error) {
|
||||||
|
lines, err := ReadFileLines(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return "", fmt.Errorf("file is empty")
|
||||||
|
}
|
||||||
|
return lines[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseKeyValue parses a key=value string
|
||||||
|
func ParseKeyValue(line string) (string, string, bool) {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
// Remove quotes if present
|
||||||
|
value = strings.Trim(value, `"'`)
|
||||||
|
return key, value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseInt64 safely parses a string to int64
|
||||||
|
func ParseInt64(s string) int64 {
|
||||||
|
val, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseInt safely parses a string to int
|
||||||
|
func ParseInt(s string) int {
|
||||||
|
val, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRoot checks if the current user is root
|
||||||
|
func IsRoot() bool {
|
||||||
|
return os.Geteuid() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand executes a command and returns its output
|
||||||
|
func RunCommand(name string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommandWithInput executes a command with stdin input
|
||||||
|
func RunCommandWithInput(input, name string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Stdin = strings.NewReader(input)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandExists checks if a command exists in PATH
|
||||||
|
func CommandExists(cmd string) bool {
|
||||||
|
_, err := exec.LookPath(cmd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserConfirmation prompts user for yes/no confirmation
|
||||||
|
func GetUserConfirmation(prompt string) bool {
|
||||||
|
fmt.Printf("%s (y/N): ", prompt)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
return response == "y" || response == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInput prompts user for input with a default value
|
||||||
|
func GetUserInput(prompt, defaultValue string) string {
|
||||||
|
if defaultValue != "" {
|
||||||
|
fmt.Printf("%s [%s]: ", prompt, defaultValue)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: ", prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var input string
|
||||||
|
fmt.Scanln(&input)
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
if input == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureRoot ensures the program is running with root privileges
|
||||||
|
func EnsureRoot() error {
|
||||||
|
if !IsRoot() {
|
||||||
|
return fmt.Errorf("this operation requires root privileges")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestElevation requests privilege elevation using sudo
|
||||||
|
func RequestElevation(args []string) error {
|
||||||
|
if IsRoot() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("This operation requires elevated privileges.")
|
||||||
|
if !GetUserConfirmation("Continue with sudo?") {
|
||||||
|
return fmt.Errorf("operation cancelled by user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare sudo command
|
||||||
|
sudoArgs := append([]string{os.Args[0]}, args...)
|
||||||
|
cmd := exec.Command("sudo", sudoArgs...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// Execute with sudo
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||||
|
os.Exit(status.ExitStatus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to execute with elevated privileges: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit successfully since the elevated process completed
|
||||||
|
os.Exit(0)
|
||||||
|
return nil // This line will never be reached
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeInput sanitizes user input by removing potentially dangerous characters
|
||||||
|
func SanitizeInput(input string) string {
|
||||||
|
// Remove null bytes and control characters
|
||||||
|
sanitized := strings.ReplaceAll(input, "\x00", "")
|
||||||
|
sanitized = strings.TrimSpace(sanitized)
|
||||||
|
|
||||||
|
// Basic validation - reject inputs with shell metacharacters
|
||||||
|
dangerous := []string{";", "&", "|", "`", "$", "(", ")", "<", ">", "\"", "'"}
|
||||||
|
for _, char := range dangerous {
|
||||||
|
if strings.Contains(sanitized, char) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatBytes formats bytes into human-readable format
|
||||||
|
func FormatBytes(bytes int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
|
||||||
|
units := []string{"KB", "MB", "GB", "TB", "PB"}
|
||||||
|
return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks if a slice contains a string
|
||||||
|
func Contains(slice []string, item string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveEmpty removes empty strings from a slice
|
||||||
|
func RemoveEmpty(slice []string) []string {
|
||||||
|
var result []string
|
||||||
|
for _, s := range slice {
|
||||||
|
if strings.TrimSpace(s) != "" {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
174
pkg/utils/helpers_test.go
Normal file
174
pkg/utils/helpers_test.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileExists(t *testing.T) {
|
||||||
|
tempFile, err := os.CreateTemp("", "test_file")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
if !FileExists(tempFile.Name()) {
|
||||||
|
t.Errorf("FileExists should return true for existing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if FileExists("/non/existent/file") {
|
||||||
|
t.Errorf("FileExists should return false for non-existing file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKeyValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expectedKey string
|
||||||
|
expectedVal string
|
||||||
|
expectedOk bool
|
||||||
|
}{
|
||||||
|
{"key=value", "key", "value", true},
|
||||||
|
{"key = value", "key", "value", true},
|
||||||
|
{"key=\"quoted value\"", "key", "quoted value", true},
|
||||||
|
{"key='single quoted'", "key", "single quoted", true},
|
||||||
|
{"invalid_line", "", "", false},
|
||||||
|
{"key=", "key", "", true},
|
||||||
|
{"=value", "", "value", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
key, value, ok := ParseKeyValue(test.input)
|
||||||
|
if key != test.expectedKey || value != test.expectedVal || ok != test.expectedOk {
|
||||||
|
t.Errorf("ParseKeyValue(%q) = (%q, %q, %v), want (%q, %q, %v)",
|
||||||
|
test.input, key, value, ok, test.expectedKey, test.expectedVal, test.expectedOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseInt64(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected int64
|
||||||
|
}{
|
||||||
|
{"123", 123},
|
||||||
|
{"0", 0},
|
||||||
|
{"-456", -456},
|
||||||
|
{"invalid", 0},
|
||||||
|
{"", 0},
|
||||||
|
{"123.45", 0}, // Should fail for float
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := ParseInt64(test.input)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("ParseInt64(%q) = %d, want %d", test.input, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseInt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"123", 123},
|
||||||
|
{"0", 0},
|
||||||
|
{"-456", -456},
|
||||||
|
{"invalid", 0},
|
||||||
|
{"", 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := ParseInt(test.input)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("ParseInt(%q) = %d, want %d", test.input, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"clean_input", "clean_input"},
|
||||||
|
{" spaced ", "spaced"},
|
||||||
|
{"with;semicolon", ""},
|
||||||
|
{"with&ersand", ""},
|
||||||
|
{"with|pipe", ""},
|
||||||
|
{"with`backtick", ""},
|
||||||
|
{"with$dollar", ""},
|
||||||
|
{"with(paren", ""},
|
||||||
|
{"with\"quote", ""},
|
||||||
|
{"with'quote", ""},
|
||||||
|
{"normal_text_123", "normal_text_123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := SanitizeInput(test.input)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("SanitizeInput(%q) = %q, want %q", test.input, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatBytes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input int64
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{500, "500 B"},
|
||||||
|
{1024, "1.0 KB"},
|
||||||
|
{1536, "1.5 KB"},
|
||||||
|
{1048576, "1.0 MB"},
|
||||||
|
{1073741824, "1.0 GB"},
|
||||||
|
{0, "0 B"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := FormatBytes(test.input)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("FormatBytes(%d) = %q, want %q", test.input, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains(t *testing.T) {
|
||||||
|
slice := []string{"apple", "banana", "cherry"}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
item string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"apple", true},
|
||||||
|
{"banana", true},
|
||||||
|
{"cherry", true},
|
||||||
|
{"grape", false},
|
||||||
|
{"", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := Contains(slice, test.item)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("Contains(slice, %q) = %v, want %v", test.item, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveEmpty(t *testing.T) {
|
||||||
|
input := []string{"apple", "", "banana", " ", "cherry", "\t\n"}
|
||||||
|
expected := []string{"apple", "banana", "cherry"}
|
||||||
|
|
||||||
|
result := RemoveEmpty(input)
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Fatalf("RemoveEmpty() returned slice of length %d, want %d", len(result), len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range result {
|
||||||
|
if item != expected[i] {
|
||||||
|
t.Errorf("RemoveEmpty()[%d] = %q, want %q", i, item, expected[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
pkg/utils/logger.go
Normal file
83
pkg/utils/logger.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger provides structured logging capabilities
|
||||||
|
type Logger struct {
|
||||||
|
*slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger creates a new structured logger
|
||||||
|
func NewLogger() *Logger {
|
||||||
|
return NewLoggerWithOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDebugLogger creates a new logger with debug level enabled
|
||||||
|
func NewDebugLogger() *Logger {
|
||||||
|
opts := &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
// Customize timestamp format
|
||||||
|
if a.Key == slog.TimeKey {
|
||||||
|
return slog.Attr{
|
||||||
|
Key: a.Key,
|
||||||
|
Value: slog.StringValue(time.Now().Format("2006-01-02 15:04:05")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := slog.NewTextHandler(os.Stdout, opts)
|
||||||
|
logger := slog.New(handler)
|
||||||
|
|
||||||
|
return &Logger{Logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoggerWithOutput creates a new logger with custom output
|
||||||
|
func NewLoggerWithOutput(w io.Writer) *Logger {
|
||||||
|
opts := &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
// Customize timestamp format
|
||||||
|
if a.Key == slog.TimeKey {
|
||||||
|
return slog.Attr{
|
||||||
|
Key: a.Key,
|
||||||
|
Value: slog.StringValue(time.Now().Format("2006-01-02 15:04:05")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := slog.NewTextHandler(w, opts)
|
||||||
|
logger := slog.New(handler)
|
||||||
|
|
||||||
|
return &Logger{Logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSilentLogger creates a logger that discards all output
|
||||||
|
func NewSilentLogger() *Logger {
|
||||||
|
return NewLoggerWithOutput(io.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithComponent adds a component context to the logger
|
||||||
|
func (l *Logger) WithComponent(component string) *Logger {
|
||||||
|
return &Logger{Logger: l.Logger.With("component", component)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestID adds a request ID context to the logger
|
||||||
|
func (l *Logger) WithRequestID(requestID string) *Logger {
|
||||||
|
return &Logger{Logger: l.Logger.With("request_id", requestID)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a fatal error and exits the program
|
||||||
|
func (l *Logger) Fatal(msg string, args ...interface{}) {
|
||||||
|
l.Error(msg, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
Reference in New Issue
Block a user