add step list for server creation
All checks were successful
Release and Deploy / build (push) Successful in 9m5s
Release and Deploy / deploy (push) Successful in 26s

This commit is contained in:
Fran Jurmanović
2025-09-18 22:24:51 +02:00
parent 901dbe697e
commit 4004d83411
80 changed files with 950 additions and 2554 deletions

View File

@@ -9,26 +9,22 @@ import (
"go.uber.org/dig"
)
// CacheItem represents an item in the cache
type CacheItem struct {
Value interface{}
Expiration int64
}
// InMemoryCache is a thread-safe in-memory cache
type InMemoryCache struct {
items map[string]CacheItem
mu sync.RWMutex
}
// NewInMemoryCache creates and returns a new InMemoryCache instance
func NewInMemoryCache() *InMemoryCache {
return &InMemoryCache{
items: make(map[string]CacheItem),
}
}
// Set adds an item to the cache with an expiration duration (in seconds)
func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
@@ -44,7 +40,6 @@ func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duratio
}
}
// Get retrieves an item from the cache
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
@@ -55,24 +50,18 @@ func (c *InMemoryCache) Get(key string) (interface{}, bool) {
}
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
// Item has expired, but don't delete here to avoid lock upgrade.
// It will be overwritten on the next Set.
return nil, false
}
return item.Value, true
}
// Delete removes an item from the cache
func (c *InMemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
// GetOrSet retrieves an item from the cache. If the item is not found, it
// calls the provided function to get the value, sets it in the cache, and
// returns it.
func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetcher func() (T, error)) (T, error) {
if cached, found := c.Get(key); found {
if value, ok := cached.(T); ok {
@@ -90,7 +79,6 @@ func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetch
return value, nil
}
// Start initializes the cache and provides it to the DI container.
func Start(di *dig.Container) {
cache := NewInMemoryCache()
err := di.Provide(func() *InMemoryCache {

View File

@@ -0,0 +1,245 @@
package command
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/logging"
"bufio"
"context"
"fmt"
"io"
"os/exec"
"strings"
"time"
"github.com/google/uuid"
)
type CallbackInteractiveCommandExecutor struct {
*InteractiveCommandExecutor
callbacks *CallbackConfig
serverID uuid.UUID
}
func NewCallbackInteractiveCommandExecutor(baseExecutor *CommandExecutor, tfaManager *model.Steam2FAManager, callbacks *CallbackConfig, serverID uuid.UUID) *CallbackInteractiveCommandExecutor {
if callbacks == nil {
callbacks = DefaultCallbackConfig()
}
return &CallbackInteractiveCommandExecutor{
InteractiveCommandExecutor: &InteractiveCommandExecutor{
CommandExecutor: baseExecutor,
tfaManager: tfaManager,
},
callbacks: callbacks,
serverID: serverID,
}
}
func (e *CallbackInteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, serverID *uuid.UUID, args ...string) error {
cmd := exec.CommandContext(ctx, e.ExePath, args...)
if e.WorkDir != "" {
cmd.Dir = e.WorkDir
}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %v", err)
}
defer stdin.Close()
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create stdout pipe: %v", err)
}
defer stdout.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed to create stderr pipe: %v", err)
}
defer stderr.Close()
logging.Info("Executing interactive command with callbacks: %s %s", e.ExePath, strings.Join(args, " "))
e.callbacks.OnCommand(e.serverID, e.ExePath, args, false, false, "")
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Starting command: %s %s", e.ExePath, strings.Join(args, " ")), false)
if err := cmd.Start(); err != nil {
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to start command: %v", err), true)
return fmt.Errorf("failed to start command: %v", err)
}
outputDone := make(chan error, 1)
cmdDone := make(chan error, 1)
go e.monitorOutputWithCallbacks(ctx, stdout, stderr, serverID, outputDone)
go func() {
cmdDone <- cmd.Wait()
}()
var cmdErr, outputErr error
completedCount := 0
for completedCount < 2 {
select {
case cmdErr = <-cmdDone:
completedCount++
logging.Info("Command execution completed")
e.callbacks.OnOutput(e.serverID, "Command execution completed", false)
case outputErr = <-outputDone:
completedCount++
logging.Info("Output monitoring completed")
case <-ctx.Done():
e.callbacks.OnOutput(e.serverID, "Command execution cancelled", true)
return ctx.Err()
}
}
if outputErr != nil {
logging.Warn("Output monitoring error: %v", outputErr)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Output monitoring error: %v", outputErr), true)
}
success := cmdErr == nil
errorMsg := ""
if cmdErr != nil {
errorMsg = cmdErr.Error()
}
e.callbacks.OnCommand(e.serverID, e.ExePath, args, true, success, errorMsg)
return cmdErr
}
func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
defer func() {
select {
case done <- nil:
default:
}
}()
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan outputLine, 100)
readersDone := make(chan struct{}, 2)
steamConsoleStarted := false
tfaRequestCreated := false
go func() {
defer func() { readersDone <- struct{}{} }()
for stdoutScanner.Scan() {
line := stdoutScanner.Text()
if e.LogOutput {
logging.Info("STDOUT: %s", line)
}
e.callbacks.OnOutput(e.serverID, line, false)
select {
case outputChan <- outputLine{text: line, isError: false}:
case <-ctx.Done():
return
}
}
if err := stdoutScanner.Err(); err != nil {
logging.Warn("Stdout scanner error: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Stdout scanner error: %v", err), true)
}
}()
go func() {
defer func() { readersDone <- struct{}{} }()
for stderrScanner.Scan() {
line := stderrScanner.Text()
if e.LogOutput {
logging.Info("STDERR: %s", line)
}
e.callbacks.OnOutput(e.serverID, line, true)
select {
case outputChan <- outputLine{text: line, isError: true}:
case <-ctx.Done():
return
}
}
if err := stderrScanner.Err(); err != nil {
logging.Warn("Stderr scanner error: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Stderr scanner error: %v", err), true)
}
}()
readersFinished := 0
for {
select {
case <-ctx.Done():
done <- ctx.Err()
return
case <-readersDone:
readersFinished++
if readersFinished == 2 {
close(outputChan)
for lineData := range outputChan {
if e.is2FAPrompt(lineData.text) {
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
done <- err
return
}
}
}
return
}
case lineData, ok := <-outputChan:
if !ok {
return
}
lowerLine := strings.ToLower(lineData.text)
if strings.Contains(lowerLine, "steam console client") && strings.Contains(lowerLine, "valve corporation") {
steamConsoleStarted = true
logging.Info("Steam Console Client startup detected - will monitor for 2FA hang")
e.callbacks.OnOutput(e.serverID, "Steam Console Client startup detected", false)
}
if e.is2FAPrompt(lineData.text) {
if !tfaRequestCreated {
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
done <- err
return
}
tfaRequestCreated = true
}
}
if tfaRequestCreated && e.isSteamContinuing(lineData.text) {
logging.Info("Steam CMD appears to have continued after 2FA confirmation")
e.callbacks.OnOutput(e.serverID, "Steam CMD continued after 2FA confirmation", false)
e.autoCompletePendingRequests(serverID)
}
case <-time.After(15 * time.Second):
if steamConsoleStarted && !tfaRequestCreated {
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
e.callbacks.OnOutput(e.serverID, "Waiting for Steam Guard 2FA confirmation...", false)
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
done <- err
return
}
tfaRequestCreated = true
}
}
}
}
type outputLine struct {
text string
isError bool
}

View File

@@ -0,0 +1,19 @@
package command
import "github.com/google/uuid"
type OutputCallback func(serverID uuid.UUID, output string, isError bool)
type CommandCallback func(serverID uuid.UUID, command string, args []string, completed bool, success bool, error string)
type CallbackConfig struct {
OnOutput OutputCallback
OnCommand CommandCallback
}
func DefaultCallbackConfig() *CallbackConfig {
return &CallbackConfig{
OnOutput: func(uuid.UUID, string, bool) {},
OnCommand: func(uuid.UUID, string, []string, bool, bool, string) {},
}
}

View File

@@ -8,17 +8,12 @@ import (
"strings"
)
// CommandExecutor provides a base structure for executing commands
type CommandExecutor struct {
// Base executable path
ExePath string
// Working directory for commands
WorkDir string
// Whether to capture and log output
ExePath string
WorkDir string
LogOutput bool
}
// CommandBuilder helps build command arguments
type CommandBuilder struct {
args []string
}
@@ -48,10 +43,9 @@ func (b *CommandBuilder) Build() []string {
return b.args
}
// Execute runs a command with the given arguments
func (e *CommandExecutor) Execute(args ...string) error {
cmd := exec.Command(e.ExePath, args...)
if e.WorkDir != "" {
cmd.Dir = e.WorkDir
}
@@ -65,15 +59,13 @@ func (e *CommandExecutor) Execute(args ...string) error {
return cmd.Run()
}
// ExecuteWithBuilder runs a command using a CommandBuilder
func (e *CommandExecutor) ExecuteWithBuilder(builder *CommandBuilder) error {
return e.Execute(builder.Build()...)
}
// ExecuteWithOutput runs a command and returns its output
func (e *CommandExecutor) ExecuteWithOutput(args ...string) (string, error) {
cmd := exec.Command(e.ExePath, args...)
if e.WorkDir != "" {
cmd.Dir = e.WorkDir
}
@@ -83,10 +75,9 @@ func (e *CommandExecutor) ExecuteWithOutput(args ...string) (string, error) {
return string(output), err
}
// ExecuteWithEnv runs a command with custom environment variables
func (e *CommandExecutor) ExecuteWithEnv(env []string, args ...string) error {
cmd := exec.Command(e.ExePath, args...)
if e.WorkDir != "" {
cmd.Dir = e.WorkDir
}
@@ -100,4 +91,4 @@ func (e *CommandExecutor) ExecuteWithEnv(env []string, args ...string) error {
logging.Info("Executing command: %s %s", e.ExePath, strings.Join(args, " "))
return cmd.Run()
}
}

View File

@@ -9,14 +9,12 @@ import (
"io"
"os"
"os/exec"
"reflect"
"strings"
"time"
"github.com/google/uuid"
)
// InteractiveCommandExecutor extends CommandExecutor to handle interactive commands
type InteractiveCommandExecutor struct {
*CommandExecutor
tfaManager *model.Steam2FAManager
@@ -29,7 +27,6 @@ func NewInteractiveCommandExecutor(baseExecutor *CommandExecutor, tfaManager *mo
}
}
// ExecuteInteractive runs a command that may require 2FA input
func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, serverID *uuid.UUID, args ...string) error {
cmd := exec.CommandContext(ctx, e.ExePath, args...)
@@ -37,7 +34,6 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
cmd.Dir = e.WorkDir
}
// Create pipes for stdin, stdout, and stderr
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %v", err)
@@ -58,7 +54,6 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
logging.Info("Executing interactive command: %s %s", e.ExePath, strings.Join(args, " "))
// Enable debug mode if environment variable is set
debugMode := os.Getenv("STEAMCMD_DEBUG") == "true"
if debugMode {
logging.Info("STEAMCMD_DEBUG mode enabled - will log all output and create proactive 2FA requests")
@@ -68,19 +63,15 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
return fmt.Errorf("failed to start command: %v", err)
}
// Create channels for output monitoring
outputDone := make(chan error, 1)
cmdDone := make(chan error, 1)
// Monitor stdout and stderr for 2FA prompts
go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone)
// Wait for the command to finish in a separate goroutine
go func() {
cmdDone <- cmd.Wait()
}()
// Wait for both command and output monitoring to complete
var cmdErr, outputErr error
completedCount := 0
@@ -112,18 +103,15 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
}
}()
// Create scanners for both outputs
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan string, 100) // Buffered channel to prevent blocking
outputChan := make(chan string, 100)
readersDone := make(chan struct{}, 2)
// Track Steam Console startup for this specific execution
steamConsoleStarted := false
tfaRequestCreated := false
// Read from stdout
go func() {
defer func() { readersDone <- struct{}{} }()
for stdoutScanner.Scan() {
@@ -131,7 +119,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
if e.LogOutput {
logging.Info("STDOUT: %s", line)
}
// Always log Steam CMD output for debugging 2FA issues
if strings.Contains(strings.ToLower(line), "steam") {
logging.Info("STEAM_DEBUG: %s", line)
}
@@ -146,7 +133,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
}
}()
// Read from stderr
go func() {
defer func() { readersDone <- struct{}{} }()
for stderrScanner.Scan() {
@@ -154,7 +140,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
if e.LogOutput {
logging.Info("STDERR: %s", line)
}
// Always log Steam CMD errors for debugging 2FA issues
if strings.Contains(strings.ToLower(line), "steam") {
logging.Info("STEAM_DEBUG_ERR: %s", line)
}
@@ -169,7 +154,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
}
}()
// Monitor for completion and 2FA prompts
readersFinished := 0
for {
select {
@@ -179,9 +163,7 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
case <-readersDone:
readersFinished++
if readersFinished == 2 {
// Both readers are done, close output channel and finish monitoring
close(outputChan)
// Drain any remaining output
for line := range outputChan {
if e.is2FAPrompt(line) {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
@@ -195,18 +177,15 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
}
case line, ok := <-outputChan:
if !ok {
// Channel closed, we're done
return
}
// Check for Steam Console startup
lowerLine := strings.ToLower(line)
if strings.Contains(lowerLine, "steam console client") && strings.Contains(lowerLine, "valve corporation") {
steamConsoleStarted = true
logging.Info("Steam Console Client startup detected - will monitor for 2FA hang")
}
// Check if this line indicates a 2FA prompt
if e.is2FAPrompt(line) {
if !tfaRequestCreated {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
@@ -218,15 +197,11 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
}
}
// Check if Steam CMD continued after 2FA (auto-completion)
if tfaRequestCreated && e.isSteamContinuing(line) {
logging.Info("Steam CMD appears to have continued after 2FA confirmation - auto-completing 2FA request")
// Auto-complete any pending 2FA requests for this server
e.autoCompletePendingRequests(serverID)
}
case <-time.After(15 * time.Second):
// If Steam Console has started and we haven't seen output for 15 seconds,
// it's very likely waiting for 2FA confirmation
if steamConsoleStarted && !tfaRequestCreated {
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
@@ -243,7 +218,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
}
func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
// Common SteamCMD 2FA prompts - updated with more comprehensive patterns
twoFAKeywords := []string{
"please enter your steam guard code",
"steam guard",
@@ -272,7 +246,6 @@ func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
}
}
// Also check for patterns that might indicate Steam is waiting for input
waitingPatterns := []string{
"waiting for",
"please enter",
@@ -330,12 +303,9 @@ func (e *InteractiveCommandExecutor) autoCompletePendingRequests(serverID *uuid.
func (e *InteractiveCommandExecutor) handle2FAPrompt(_ context.Context, promptLine string, serverID *uuid.UUID) error {
logging.Info("2FA prompt detected: %s", promptLine)
// Create a 2FA request
request := e.tfaManager.CreateRequest(promptLine, serverID)
logging.Info("Created 2FA request with ID: %s", request.ID)
// Wait for user to complete the 2FA process
// Use a reasonable timeout (e.g., 5 minutes)
timeout := 5 * time.Minute
success, err := e.tfaManager.WaitForCompletion(request.ID, timeout)
@@ -352,271 +322,3 @@ func (e *InteractiveCommandExecutor) handle2FAPrompt(_ context.Context, promptLi
logging.Info("2FA completed successfully")
return nil
}
// WebSocketInteractiveCommandExecutor extends InteractiveCommandExecutor to stream output via WebSocket
type WebSocketInteractiveCommandExecutor struct {
*InteractiveCommandExecutor
wsService interface{} // Using interface{} to avoid circular import
serverID uuid.UUID
}
// NewInteractiveCommandExecutorWithWebSocket creates a new WebSocket-enabled interactive command executor
func NewInteractiveCommandExecutorWithWebSocket(baseExecutor *CommandExecutor, tfaManager *model.Steam2FAManager, wsService interface{}, serverID uuid.UUID) *WebSocketInteractiveCommandExecutor {
return &WebSocketInteractiveCommandExecutor{
InteractiveCommandExecutor: &InteractiveCommandExecutor{
CommandExecutor: baseExecutor,
tfaManager: tfaManager,
},
wsService: wsService,
serverID: serverID,
}
}
// ExecuteInteractive runs a command with WebSocket output streaming
func (e *WebSocketInteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, serverID *uuid.UUID, args ...string) error {
cmd := exec.CommandContext(ctx, e.ExePath, args...)
if e.WorkDir != "" {
cmd.Dir = e.WorkDir
}
// Create pipes for stdin, stdout, and stderr
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %v", err)
}
defer stdin.Close()
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create stdout pipe: %v", err)
}
defer stdout.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed to create stderr pipe: %v", err)
}
defer stderr.Close()
logging.Info("Executing interactive command with WebSocket streaming: %s %s", e.ExePath, strings.Join(args, " "))
// Broadcast command start via WebSocket
e.broadcastSteamOutput(fmt.Sprintf("Starting command: %s %s", e.ExePath, strings.Join(args, " ")), false)
if err := cmd.Start(); err != nil {
e.broadcastSteamOutput(fmt.Sprintf("Failed to start command: %v", err), true)
return fmt.Errorf("failed to start command: %v", err)
}
// Create channels for output monitoring
outputDone := make(chan error, 1)
cmdDone := make(chan error, 1)
// Monitor stdout and stderr for 2FA prompts with WebSocket streaming
go e.monitorOutputWithWebSocket(ctx, stdout, stderr, serverID, outputDone)
// Wait for the command to finish in a separate goroutine
go func() {
cmdDone <- cmd.Wait()
}()
// Wait for both command and output monitoring to complete
var cmdErr, outputErr error
completedCount := 0
for completedCount < 2 {
select {
case cmdErr = <-cmdDone:
completedCount++
logging.Info("Command execution completed")
e.broadcastSteamOutput("Command execution completed", false)
case outputErr = <-outputDone:
completedCount++
logging.Info("Output monitoring completed")
case <-ctx.Done():
e.broadcastSteamOutput("Command execution cancelled", true)
return ctx.Err()
}
}
if outputErr != nil {
logging.Warn("Output monitoring error: %v", outputErr)
e.broadcastSteamOutput(fmt.Sprintf("Output monitoring error: %v", outputErr), true)
}
return cmdErr
}
// broadcastSteamOutput sends output to WebSocket using reflection to avoid circular imports
func (e *WebSocketInteractiveCommandExecutor) broadcastSteamOutput(output string, isError bool) {
if e.wsService == nil {
return
}
// Use reflection to call BroadcastSteamOutput method
wsServiceVal := reflect.ValueOf(e.wsService)
method := wsServiceVal.MethodByName("BroadcastSteamOutput")
if !method.IsValid() {
logging.Warn("BroadcastSteamOutput method not found on WebSocket service")
return
}
// Call the method with parameters: serverID, output, isError
args := []reflect.Value{
reflect.ValueOf(e.serverID),
reflect.ValueOf(output),
reflect.ValueOf(isError),
}
method.Call(args)
}
// monitorOutputWithWebSocket monitors command output and streams it via WebSocket
func (e *WebSocketInteractiveCommandExecutor) monitorOutputWithWebSocket(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
defer func() {
select {
case done <- nil:
default:
}
}()
// Create scanners for both outputs
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan outputLine, 100) // Buffered channel to prevent blocking
readersDone := make(chan struct{}, 2)
// Track Steam Console startup for this specific execution
steamConsoleStarted := false
tfaRequestCreated := false
// Read from stdout
go func() {
defer func() { readersDone <- struct{}{} }()
for stdoutScanner.Scan() {
line := stdoutScanner.Text()
if e.LogOutput {
logging.Info("STDOUT: %s", line)
}
// Stream output via WebSocket
e.broadcastSteamOutput(line, false)
select {
case outputChan <- outputLine{text: line, isError: false}:
case <-ctx.Done():
return
}
}
if err := stdoutScanner.Err(); err != nil {
logging.Warn("Stdout scanner error: %v", err)
e.broadcastSteamOutput(fmt.Sprintf("Stdout scanner error: %v", err), true)
}
}()
// Read from stderr
go func() {
defer func() { readersDone <- struct{}{} }()
for stderrScanner.Scan() {
line := stderrScanner.Text()
if e.LogOutput {
logging.Info("STDERR: %s", line)
}
// Stream error output via WebSocket
e.broadcastSteamOutput(line, true)
select {
case outputChan <- outputLine{text: line, isError: true}:
case <-ctx.Done():
return
}
}
if err := stderrScanner.Err(); err != nil {
logging.Warn("Stderr scanner error: %v", err)
e.broadcastSteamOutput(fmt.Sprintf("Stderr scanner error: %v", err), true)
}
}()
// Monitor for completion and 2FA prompts
readersFinished := 0
for {
select {
case <-ctx.Done():
done <- ctx.Err()
return
case <-readersDone:
readersFinished++
if readersFinished == 2 {
// Both readers are done, close output channel and finish monitoring
close(outputChan)
// Drain any remaining output
for lineData := range outputChan {
if e.is2FAPrompt(lineData.text) {
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
e.broadcastSteamOutput(fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
done <- err
return
}
}
}
return
}
case lineData, ok := <-outputChan:
if !ok {
// Channel closed, we're done
return
}
// Check for Steam Console startup
lowerLine := strings.ToLower(lineData.text)
if strings.Contains(lowerLine, "steam console client") && strings.Contains(lowerLine, "valve corporation") {
steamConsoleStarted = true
logging.Info("Steam Console Client startup detected - will monitor for 2FA hang")
e.broadcastSteamOutput("Steam Console Client startup detected", false)
}
// Check if this line indicates a 2FA prompt
if e.is2FAPrompt(lineData.text) {
if !tfaRequestCreated {
e.broadcastSteamOutput("2FA prompt detected - waiting for user confirmation", false)
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
e.broadcastSteamOutput(fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
done <- err
return
}
tfaRequestCreated = true
}
}
// Check if Steam CMD continued after 2FA (auto-completion)
if tfaRequestCreated && e.isSteamContinuing(lineData.text) {
logging.Info("Steam CMD appears to have continued after 2FA confirmation")
e.broadcastSteamOutput("Steam CMD continued after 2FA confirmation", false)
// Auto-complete any pending 2FA requests for this server
e.autoCompletePendingRequests(serverID)
}
case <-time.After(15 * time.Second):
// If Steam Console has started and we haven't seen output for 15 seconds,
// it's very likely waiting for 2FA confirmation
if steamConsoleStarted && !tfaRequestCreated {
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
e.broadcastSteamOutput("Waiting for Steam Guard 2FA confirmation...", false)
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
e.broadcastSteamOutput(fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
done <- err
return
}
tfaRequestCreated = true
}
}
}
}
// outputLine represents a line of output with error status
type outputLine struct {
text string
isError bool
}

View File

@@ -79,12 +79,6 @@ func IndentJson(body []byte) ([]byte, error) {
return unmarshaledBody.Bytes(), nil
}
// ParseQueryFilter parses query parameters into a filter struct using reflection.
// It supports various field types and uses struct tags to determine parsing behavior.
// Supported tags:
// - `query:"field_name"` - specifies the query parameter name
// - `param:"param_name"` - specifies the path parameter name
// - `time_format:"format"` - specifies the time format for parsing dates (default: RFC3339)
func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
val := reflect.ValueOf(filter)
if val.Kind() != reflect.Ptr || val.IsNil() {
@@ -94,14 +88,12 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
elem := val.Elem()
typ := elem.Type()
// Process all fields including embedded structs
var processFields func(reflect.Value, reflect.Type) error
processFields = func(val reflect.Value, typ reflect.Type) error {
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// Handle embedded structs recursively
if fieldType.Anonymous {
if err := processFields(field, fieldType.Type); err != nil {
return err
@@ -109,12 +101,10 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
continue
}
// Skip if field cannot be set
if !field.CanSet() {
continue
}
// Check for param tag first (path parameters)
if paramName := fieldType.Tag.Get("param"); paramName != "" {
if err := parsePathParam(c, field, paramName); err != nil {
return fmt.Errorf("error parsing path parameter %s: %v", paramName, err)
@@ -122,15 +112,14 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
continue
}
// Then check for query tag
queryName := fieldType.Tag.Get("query")
if queryName == "" {
queryName = ToSnakeCase(fieldType.Name) // Default to snake_case of field name
queryName = ToSnakeCase(fieldType.Name)
}
queryVal := c.Query(queryName)
if queryVal == "" {
continue // Skip empty values
continue
}
if err := parseValue(field, queryVal, fieldType.Tag); err != nil {

View File

@@ -18,7 +18,6 @@ var (
func Init() {
godotenv.Load()
// Fail fast if critical environment variables are missing
Secret = getEnvRequired("APP_SECRET")
SecretCode = getEnvRequired("APP_SECRET_CODE")
EncryptionKey = getEnvRequired("ENCRYPTION_KEY")
@@ -29,7 +28,6 @@ func Init() {
}
}
// getEnv retrieves an environment variable or returns a fallback value.
func getEnv(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
@@ -38,12 +36,10 @@ func getEnv(key, fallback string) string {
return fallback
}
// getEnvRequired retrieves an environment variable and fails if it's not set.
// This should be used for critical configuration that must not have defaults.
func getEnvRequired(key string) string {
if value, exists := os.LookupEnv(key); exists && value != "" {
return value
}
log.Fatalf("Required environment variable %s is not set or is empty", key)
return "" // This line will never be reached due to log.Fatalf
return ""
}

View File

@@ -33,7 +33,6 @@ func Start(di *dig.Container) {
func Migrate(db *gorm.DB) {
logging.Info("Migrating database")
// Run GORM AutoMigrate for all models
err := db.AutoMigrate(
&model.ServiceControlModel{},
&model.Config{},
@@ -52,7 +51,6 @@ func Migrate(db *gorm.DB) {
if err != nil {
logging.Error("GORM AutoMigrate failed: %v", err)
// Don't panic, just log the error as custom migrations may have handled this
}
db.FirstOrCreate(&model.ServiceControlModel{ServiceControl: "Works"})
@@ -63,10 +61,8 @@ func Migrate(db *gorm.DB) {
func runMigrations(db *gorm.DB) {
logging.Info("Running custom database migrations...")
// Migration 001: Password security upgrade
if err := migrations.RunPasswordSecurityMigration(db); err != nil {
logging.Error("Failed to run password security migration: %v", err)
// Continue - this migration might not be needed for all setups
}
logging.Info("Custom database migrations completed")
@@ -132,7 +128,6 @@ func seedCarModels(db *gorm.DB) error {
carModels := []model.CarModel{
{Value: 0, CarModel: "Porsche 991 GT3 R"},
{Value: 1, CarModel: "Mercedes-AMG GT3"},
// ... Add all car models from your list
}
for _, cm := range carModels {

View File

@@ -6,12 +6,10 @@ import (
)
const (
// Default paths for when environment variables are not set
DefaultSteamCMDPath = "c:\\steamcmd\\steamcmd.exe"
DefaultNSSMPath = ".\\nssm.exe"
)
// GetSteamCMDPath returns the SteamCMD executable path from environment variable or default
func GetSteamCMDPath() string {
if path := os.Getenv("STEAMCMD_PATH"); path != "" {
return path
@@ -19,13 +17,11 @@ func GetSteamCMDPath() string {
return DefaultSteamCMDPath
}
// GetSteamCMDDirPath returns the directory containing SteamCMD executable
func GetSteamCMDDirPath() string {
steamCMDPath := GetSteamCMDPath()
return filepath.Dir(steamCMDPath)
}
// GetNSSMPath returns the NSSM executable path from environment variable or default
func GetNSSMPath() string {
if path := os.Getenv("NSSM_PATH"); path != "" {
return path
@@ -33,17 +29,14 @@ func GetNSSMPath() string {
return DefaultNSSMPath
}
// ValidatePaths checks if the configured paths exist (optional validation)
func ValidatePaths() map[string]error {
errors := make(map[string]error)
// Check SteamCMD path
steamCMDPath := GetSteamCMDPath()
if _, err := os.Stat(steamCMDPath); os.IsNotExist(err) {
errors["STEAMCMD_PATH"] = err
}
// Check NSSM path
nssmPath := GetNSSMPath()
if _, err := os.Stat(nssmPath); os.IsNotExist(err) {
errors["NSSM_PATH"] = err

View File

@@ -9,45 +9,37 @@ import (
"github.com/gofiber/fiber/v2"
)
// ControllerErrorHandler provides centralized error handling for controllers
type ControllerErrorHandler struct {
errorLogger *logging.ErrorLogger
}
// NewControllerErrorHandler creates a new controller error handler instance
func NewControllerErrorHandler() *ControllerErrorHandler {
return &ControllerErrorHandler{
errorLogger: logging.GetErrorLogger(),
}
}
// ErrorResponse represents a standardized error response
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code,omitempty"`
Details map[string]string `json:"details,omitempty"`
}
// HandleError handles controller errors with logging and standardized responses
func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCode int, context ...string) error {
if err == nil {
return nil
}
// Get caller information for logging
_, file, line, _ := runtime.Caller(1)
file = strings.TrimPrefix(file, "acc-server-manager/")
// Build context string
contextStr := ""
if len(context) > 0 {
contextStr = fmt.Sprintf("[%s] ", strings.Join(context, "|"))
}
// Clean error message (remove null bytes)
cleanErrorMsg := strings.ReplaceAll(err.Error(), "\x00", "")
// Log the error with context
ceh.errorLogger.LogWithContext(
fmt.Sprintf("CONTROLLER_ERROR [%s:%d]", file, line),
"%s%s",
@@ -55,31 +47,26 @@ func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCo
cleanErrorMsg,
)
// Create standardized error response
errorResponse := ErrorResponse{
Error: cleanErrorMsg,
Code: statusCode,
}
// Add request details if available
if c != nil {
if errorResponse.Details == nil {
errorResponse.Details = make(map[string]string)
}
// Safely extract request details
func() {
defer func() {
if r := recover(); r != nil {
// If any of these panic, just skip adding the details
return
}
}()
errorResponse.Details["method"] = c.Method()
errorResponse.Details["path"] = c.Path()
// Safely get IP address
if ip := c.IP(); ip != "" {
errorResponse.Details["ip"] = ip
} else {
@@ -88,14 +75,11 @@ func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCo
}()
}
// Return appropriate response based on status code
if c == nil {
// If context is nil, we can't return a response
return fmt.Errorf("cannot return HTTP response: context is nil")
}
if statusCode >= 500 {
// For server errors, don't expose internal details
return c.Status(statusCode).JSON(ErrorResponse{
Error: "Internal server error",
Code: statusCode,
@@ -105,52 +89,42 @@ func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCo
return c.Status(statusCode).JSON(errorResponse)
}
// HandleValidationError handles validation errors specifically
func (ceh *ControllerErrorHandler) HandleValidationError(c *fiber.Ctx, err error, field string) error {
return ceh.HandleError(c, err, fiber.StatusBadRequest, "VALIDATION", field)
}
// HandleDatabaseError handles database-related errors
func (ceh *ControllerErrorHandler) HandleDatabaseError(c *fiber.Ctx, err error) error {
return ceh.HandleError(c, err, fiber.StatusInternalServerError, "DATABASE")
}
// HandleAuthError handles authentication/authorization errors
func (ceh *ControllerErrorHandler) HandleAuthError(c *fiber.Ctx, err error) error {
return ceh.HandleError(c, err, fiber.StatusUnauthorized, "AUTH")
}
// HandleNotFoundError handles resource not found errors
func (ceh *ControllerErrorHandler) HandleNotFoundError(c *fiber.Ctx, resource string) error {
err := fmt.Errorf("%s not found", resource)
return ceh.HandleError(c, err, fiber.StatusNotFound, "NOT_FOUND")
}
// HandleBusinessLogicError handles business logic errors
func (ceh *ControllerErrorHandler) HandleBusinessLogicError(c *fiber.Ctx, err error) error {
return ceh.HandleError(c, err, fiber.StatusBadRequest, "BUSINESS_LOGIC")
}
// HandleServiceError handles service layer errors
func (ceh *ControllerErrorHandler) HandleServiceError(c *fiber.Ctx, err error) error {
return ceh.HandleError(c, err, fiber.StatusInternalServerError, "SERVICE")
}
// HandleParsingError handles request parsing errors
func (ceh *ControllerErrorHandler) HandleParsingError(c *fiber.Ctx, err error) error {
return ceh.HandleError(c, err, fiber.StatusBadRequest, "PARSING")
}
// HandleUUIDError handles UUID parsing errors
func (ceh *ControllerErrorHandler) HandleUUIDError(c *fiber.Ctx, field string) error {
err := fmt.Errorf("invalid %s format", field)
return ceh.HandleError(c, err, fiber.StatusBadRequest, "UUID_VALIDATION", field)
}
// Global controller error handler instance
var globalErrorHandler *ControllerErrorHandler
// GetControllerErrorHandler returns the global controller error handler instance
func GetControllerErrorHandler() *ControllerErrorHandler {
if globalErrorHandler == nil {
globalErrorHandler = NewControllerErrorHandler()
@@ -158,49 +132,38 @@ func GetControllerErrorHandler() *ControllerErrorHandler {
return globalErrorHandler
}
// Convenience functions using the global error handler
// HandleError handles controller errors using the global error handler
func HandleError(c *fiber.Ctx, err error, statusCode int, context ...string) error {
return GetControllerErrorHandler().HandleError(c, err, statusCode, context...)
}
// HandleValidationError handles validation errors using the global error handler
func HandleValidationError(c *fiber.Ctx, err error, field string) error {
return GetControllerErrorHandler().HandleValidationError(c, err, field)
}
// HandleDatabaseError handles database errors using the global error handler
func HandleDatabaseError(c *fiber.Ctx, err error) error {
return GetControllerErrorHandler().HandleDatabaseError(c, err)
}
// HandleAuthError handles auth errors using the global error handler
func HandleAuthError(c *fiber.Ctx, err error) error {
return GetControllerErrorHandler().HandleAuthError(c, err)
}
// HandleNotFoundError handles not found errors using the global error handler
func HandleNotFoundError(c *fiber.Ctx, resource string) error {
return GetControllerErrorHandler().HandleNotFoundError(c, resource)
}
// HandleBusinessLogicError handles business logic errors using the global error handler
func HandleBusinessLogicError(c *fiber.Ctx, err error) error {
return GetControllerErrorHandler().HandleBusinessLogicError(c, err)
}
// HandleServiceError handles service errors using the global error handler
func HandleServiceError(c *fiber.Ctx, err error) error {
return GetControllerErrorHandler().HandleServiceError(c, err)
}
// HandleParsingError handles parsing errors using the global error handler
func HandleParsingError(c *fiber.Ctx, err error) error {
return GetControllerErrorHandler().HandleParsingError(c, err)
}
// HandleUUIDError handles UUID errors using the global error handler
func HandleUUIDError(c *fiber.Ctx, field string) error {
return GetControllerErrorHandler().HandleUUIDError(c, field)
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/golang-jwt/jwt/v4"
)
// Claims represents the JWT claims.
type Claims struct {
UserID string `json:"user_id"`
IsOpenToken bool `json:"is_open_token"`
@@ -27,7 +26,6 @@ type OpenJWTHandler struct {
*JWTHandler
}
// NewJWTHandler creates a new JWTHandler instance with the provided secret key.
func NewOpenJWTHandler(jwtSecret string) *OpenJWTHandler {
jwtHandler := NewJWTHandler(jwtSecret)
jwtHandler.IsOpenToken = true
@@ -36,7 +34,6 @@ func NewOpenJWTHandler(jwtSecret string) *OpenJWTHandler {
}
}
// NewJWTHandler creates a new JWTHandler instance with the provided secret key.
func NewJWTHandler(jwtSecret string) *JWTHandler {
if jwtSecret == "" {
errors.SafeFatal("JWT_SECRET environment variable is required and cannot be empty")
@@ -44,14 +41,12 @@ func NewJWTHandler(jwtSecret string) *JWTHandler {
var secretKey []byte
// Decode base64 secret if it looks like base64, otherwise use as-is
if decoded, err := base64.StdEncoding.DecodeString(jwtSecret); err == nil && len(decoded) >= 32 {
secretKey = decoded
} else {
secretKey = []byte(jwtSecret)
}
// Ensure minimum key length for security
if len(secretKey) < 32 {
errors.SafeFatal("JWT_SECRET must be at least 32 bytes long for security")
}
@@ -60,8 +55,6 @@ func NewJWTHandler(jwtSecret string) *JWTHandler {
}
}
// GenerateSecretKey generates a cryptographically secure random key for JWT signing
// This is a utility function for generating new secrets, not used in normal operation
func (jh *JWTHandler) GenerateSecretKey() string {
key := make([]byte, 64) // 512 bits
if _, err := rand.Read(key); err != nil {
@@ -70,7 +63,6 @@ func (jh *JWTHandler) GenerateSecretKey() string {
return base64.StdEncoding.EncodeToString(key)
}
// GenerateToken generates a new JWT for a given user.
func (jh *JWTHandler) GenerateToken(userId string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
@@ -99,7 +91,6 @@ func (jh *JWTHandler) GenerateTokenWithExpiry(user *model.User, expiry time.Time
return token.SignedString(jh.SecretKey)
}
// ValidateToken validates a JWT and returns the claims if the token is valid.
func (jh *JWTHandler) ValidateToken(tokenString string) (*Claims, error) {
claims := &Claims{}

View File

@@ -15,7 +15,6 @@ var (
timeFormat = "2006-01-02 15:04:05.000"
)
// BaseLogger provides the core logging functionality
type BaseLogger struct {
file *os.File
logger *log.Logger
@@ -23,7 +22,6 @@ type BaseLogger struct {
initialized bool
}
// LogLevel represents different logging levels
type LogLevel string
const (
@@ -34,28 +32,23 @@ const (
LogLevelPanic LogLevel = "PANIC"
)
// Initialize creates a new base logger instance
func InitializeBase(tp string) (*BaseLogger, error) {
return newBaseLogger(tp)
}
func newBaseLogger(tp string) (*BaseLogger, error) {
// Ensure logs directory exists
if err := os.MkdirAll("logs", 0755); err != nil {
return nil, fmt.Errorf("failed to create logs directory: %v", err)
}
// Open log file with date in name
logPath := filepath.Join("logs", fmt.Sprintf("acc-server-%s-%s.log", time.Now().Format("2006-01-02"), tp))
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %v", err)
}
// Create multi-writer for both file and console
multiWriter := io.MultiWriter(file, os.Stdout)
// Create base logger
logger := &BaseLogger{
file: file,
logger: log.New(multiWriter, "", 0),
@@ -65,13 +58,11 @@ func newBaseLogger(tp string) (*BaseLogger, error) {
return logger, nil
}
// GetBaseLogger creates and returns a new base logger instance
func GetBaseLogger(tp string) *BaseLogger {
baseLogger, _ := InitializeBase(tp)
return baseLogger
}
// Close closes the log file
func (bl *BaseLogger) Close() error {
bl.mu.Lock()
defer bl.mu.Unlock()
@@ -82,7 +73,6 @@ func (bl *BaseLogger) Close() error {
return nil
}
// Log writes a log entry with the specified level
func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
if bl == nil || !bl.initialized {
return
@@ -91,14 +81,11 @@ func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
bl.mu.RLock()
defer bl.mu.RUnlock()
// Get caller info (skip 2 frames: this function and the calling Log function)
_, file, line, _ := runtime.Caller(2)
file = filepath.Base(file)
// Format message
msg := fmt.Sprintf(format, v...)
// Format final log line
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s",
time.Now().Format(timeFormat),
string(level),
@@ -110,7 +97,6 @@ func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
bl.logger.Println(logLine)
}
// LogWithCaller writes a log entry with custom caller depth
func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format string, v ...interface{}) {
if bl == nil || !bl.initialized {
return
@@ -119,14 +105,11 @@ func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format stri
bl.mu.RLock()
defer bl.mu.RUnlock()
// Get caller info with custom depth
_, file, line, _ := runtime.Caller(callerDepth)
file = filepath.Base(file)
// Format message
msg := fmt.Sprintf(format, v...)
// Format final log line
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s",
time.Now().Format(timeFormat),
string(level),
@@ -138,7 +121,6 @@ func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format stri
bl.logger.Println(logLine)
}
// IsInitialized returns whether the base logger is initialized
func (bl *BaseLogger) IsInitialized() bool {
if bl == nil {
return false
@@ -148,19 +130,16 @@ func (bl *BaseLogger) IsInitialized() bool {
return bl.initialized
}
// RecoverAndLog recovers from panics and logs them
func RecoverAndLog() {
baseLogger := GetBaseLogger("panic")
if baseLogger != nil && baseLogger.IsInitialized() {
if r := recover(); r != nil {
// Get stack trace
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
stackTrace := string(buf[:n])
baseLogger.LogWithCaller(LogLevelPanic, 2, "Recovered from panic: %v\nStack Trace:\n%s", r, stackTrace)
// Re-panic to maintain original behavior if needed
panic(r)
}
}

View File

@@ -6,12 +6,10 @@ import (
"sync"
)
// DebugLogger handles debug-level logging
type DebugLogger struct {
base *BaseLogger
}
// NewDebugLogger creates a new debug logger instance
func NewDebugLogger() *DebugLogger {
base, _ := InitializeBase("debug")
return &DebugLogger{
@@ -19,14 +17,12 @@ func NewDebugLogger() *DebugLogger {
}
}
// Log writes a debug-level log entry
func (dl *DebugLogger) Log(format string, v ...interface{}) {
if dl.base != nil {
dl.base.Log(LogLevelDebug, format, v...)
}
}
// LogWithContext writes a debug-level log entry with additional context
func (dl *DebugLogger) LogWithContext(context string, format string, v ...interface{}) {
if dl.base != nil {
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
@@ -34,7 +30,6 @@ func (dl *DebugLogger) LogWithContext(context string, format string, v ...interf
}
}
// LogFunction logs function entry and exit for debugging
func (dl *DebugLogger) LogFunction(functionName string, args ...interface{}) {
if dl.base != nil {
if len(args) > 0 {
@@ -45,21 +40,18 @@ func (dl *DebugLogger) LogFunction(functionName string, args ...interface{}) {
}
}
// LogVariable logs variable values for debugging
func (dl *DebugLogger) LogVariable(varName string, value interface{}) {
if dl.base != nil {
dl.base.Log(LogLevelDebug, "VARIABLE [%s]: %+v", varName, value)
}
}
// LogState logs application state information
func (dl *DebugLogger) LogState(component string, state interface{}) {
if dl.base != nil {
dl.base.Log(LogLevelDebug, "STATE [%s]: %+v", component, state)
}
}
// LogSQL logs SQL queries for debugging
func (dl *DebugLogger) LogSQL(query string, args ...interface{}) {
if dl.base != nil {
if len(args) > 0 {
@@ -70,7 +62,6 @@ func (dl *DebugLogger) LogSQL(query string, args ...interface{}) {
}
}
// LogMemory logs memory usage information
func (dl *DebugLogger) LogMemory() {
if dl.base != nil {
var m runtime.MemStats
@@ -80,32 +71,27 @@ func (dl *DebugLogger) LogMemory() {
}
}
// LogGoroutines logs current number of goroutines
func (dl *DebugLogger) LogGoroutines() {
if dl.base != nil {
dl.base.Log(LogLevelDebug, "GOROUTINES: %d active", runtime.NumGoroutine())
}
}
// LogTiming logs timing information for performance debugging
func (dl *DebugLogger) LogTiming(operation string, duration interface{}) {
if dl.base != nil {
dl.base.Log(LogLevelDebug, "TIMING [%s]: %v", operation, duration)
}
}
// Helper function to convert bytes to kilobytes
func bToKb(b uint64) uint64 {
return b / 1024
}
// Global debug logger instance
var (
debugLogger *DebugLogger
debugOnce sync.Once
)
// GetDebugLogger returns the global debug logger instance
func GetDebugLogger() *DebugLogger {
debugOnce.Do(func() {
debugLogger = NewDebugLogger()
@@ -113,47 +99,38 @@ func GetDebugLogger() *DebugLogger {
return debugLogger
}
// Debug logs a debug-level message using the global debug logger
func Debug(format string, v ...interface{}) {
GetDebugLogger().Log(format, v...)
}
// DebugWithContext logs a debug-level message with context using the global debug logger
func DebugWithContext(context string, format string, v ...interface{}) {
GetDebugLogger().LogWithContext(context, format, v...)
}
// DebugFunction logs function entry and exit using the global debug logger
func DebugFunction(functionName string, args ...interface{}) {
GetDebugLogger().LogFunction(functionName, args...)
}
// DebugVariable logs variable values using the global debug logger
func DebugVariable(varName string, value interface{}) {
GetDebugLogger().LogVariable(varName, value)
}
// DebugState logs application state information using the global debug logger
func DebugState(component string, state interface{}) {
GetDebugLogger().LogState(component, state)
}
// DebugSQL logs SQL queries using the global debug logger
func DebugSQL(query string, args ...interface{}) {
GetDebugLogger().LogSQL(query, args...)
}
// DebugMemory logs memory usage information using the global debug logger
func DebugMemory() {
GetDebugLogger().LogMemory()
}
// DebugGoroutines logs current number of goroutines using the global debug logger
func DebugGoroutines() {
GetDebugLogger().LogGoroutines()
}
// DebugTiming logs timing information using the global debug logger
func DebugTiming(operation string, duration interface{}) {
GetDebugLogger().LogTiming(operation, duration)
}

View File

@@ -6,12 +6,10 @@ import (
"sync"
)
// ErrorLogger handles error-level logging
type ErrorLogger struct {
base *BaseLogger
}
// NewErrorLogger creates a new error logger instance
func NewErrorLogger() *ErrorLogger {
base, _ := InitializeBase("error")
return &ErrorLogger{
@@ -19,14 +17,12 @@ func NewErrorLogger() *ErrorLogger {
}
}
// Log writes an error-level log entry
func (el *ErrorLogger) Log(format string, v ...interface{}) {
if el.base != nil {
el.base.Log(LogLevelError, format, v...)
}
}
// LogWithContext writes an error-level log entry with additional context
func (el *ErrorLogger) LogWithContext(context string, format string, v ...interface{}) {
if el.base != nil {
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
@@ -34,7 +30,6 @@ func (el *ErrorLogger) LogWithContext(context string, format string, v ...interf
}
}
// LogError logs an error object with optional message
func (el *ErrorLogger) LogError(err error, message ...string) {
if el.base != nil && err != nil {
if len(message) > 0 {
@@ -45,7 +40,6 @@ func (el *ErrorLogger) LogError(err error, message ...string) {
}
}
// LogWithStackTrace logs an error with stack trace
func (el *ErrorLogger) LogWithStackTrace(format string, v ...interface{}) {
if el.base != nil {
// Get stack trace
@@ -58,7 +52,6 @@ func (el *ErrorLogger) LogWithStackTrace(format string, v ...interface{}) {
}
}
// LogFatal logs a fatal error and exits the program
func (el *ErrorLogger) LogFatal(format string, v ...interface{}) {
if el.base != nil {
el.base.Log(LogLevelError, "[FATAL] "+format, v...)
@@ -66,13 +59,11 @@ func (el *ErrorLogger) LogFatal(format string, v ...interface{}) {
}
}
// Global error logger instance
var (
errorLogger *ErrorLogger
errorOnce sync.Once
)
// GetErrorLogger returns the global error logger instance
func GetErrorLogger() *ErrorLogger {
errorOnce.Do(func() {
errorLogger = NewErrorLogger()
@@ -80,27 +71,22 @@ func GetErrorLogger() *ErrorLogger {
return errorLogger
}
// Error logs an error-level message using the global error logger
func Error(format string, v ...interface{}) {
GetErrorLogger().Log(format, v...)
}
// ErrorWithContext logs an error-level message with context using the global error logger
func ErrorWithContext(context string, format string, v ...interface{}) {
GetErrorLogger().LogWithContext(context, format, v...)
}
// LogError logs an error object using the global error logger
func LogError(err error, message ...string) {
GetErrorLogger().LogError(err, message...)
}
// ErrorWithStackTrace logs an error with stack trace using the global error logger
func ErrorWithStackTrace(format string, v ...interface{}) {
GetErrorLogger().LogWithStackTrace(format, v...)
}
// Fatal logs a fatal error and exits the program using the global error logger
func Fatal(format string, v ...interface{}) {
GetErrorLogger().LogFatal(format, v...)
}

View File

@@ -5,12 +5,10 @@ import (
"sync"
)
// InfoLogger handles info-level logging
type InfoLogger struct {
base *BaseLogger
}
// NewInfoLogger creates a new info logger instance
func NewInfoLogger() *InfoLogger {
base, _ := InitializeBase("info")
return &InfoLogger{
@@ -18,14 +16,12 @@ func NewInfoLogger() *InfoLogger {
}
}
// Log writes an info-level log entry
func (il *InfoLogger) Log(format string, v ...interface{}) {
if il.base != nil {
il.base.Log(LogLevelInfo, format, v...)
}
}
// LogWithContext writes an info-level log entry with additional context
func (il *InfoLogger) LogWithContext(context string, format string, v ...interface{}) {
if il.base != nil {
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
@@ -33,55 +29,47 @@ func (il *InfoLogger) LogWithContext(context string, format string, v ...interfa
}
}
// LogStartup logs application startup information
func (il *InfoLogger) LogStartup(component string, message string) {
if il.base != nil {
il.base.Log(LogLevelInfo, "STARTUP [%s]: %s", component, message)
}
}
// LogShutdown logs application shutdown information
func (il *InfoLogger) LogShutdown(component string, message string) {
if il.base != nil {
il.base.Log(LogLevelInfo, "SHUTDOWN [%s]: %s", component, message)
}
}
// LogOperation logs general operation information
func (il *InfoLogger) LogOperation(operation string, details string) {
if il.base != nil {
il.base.Log(LogLevelInfo, "OPERATION [%s]: %s", operation, details)
}
}
// LogStatus logs status changes or updates
func (il *InfoLogger) LogStatus(component string, status string) {
if il.base != nil {
il.base.Log(LogLevelInfo, "STATUS [%s]: %s", component, status)
}
}
// LogRequest logs incoming requests
func (il *InfoLogger) LogRequest(method string, path string, userAgent string) {
if il.base != nil {
il.base.Log(LogLevelInfo, "REQUEST [%s %s] User-Agent: %s", method, path, userAgent)
}
}
// LogResponse logs outgoing responses
func (il *InfoLogger) LogResponse(method string, path string, statusCode int, duration string) {
if il.base != nil {
il.base.Log(LogLevelInfo, "RESPONSE [%s %s] Status: %d, Duration: %s", method, path, statusCode, duration)
}
}
// Global info logger instance
var (
infoLogger *InfoLogger
infoOnce sync.Once
)
// GetInfoLogger returns the global info logger instance
func GetInfoLogger() *InfoLogger {
infoOnce.Do(func() {
infoLogger = NewInfoLogger()
@@ -89,42 +77,34 @@ func GetInfoLogger() *InfoLogger {
return infoLogger
}
// Info logs an info-level message using the global info logger
func Info(format string, v ...interface{}) {
GetInfoLogger().Log(format, v...)
}
// InfoWithContext logs an info-level message with context using the global info logger
func InfoWithContext(context string, format string, v ...interface{}) {
GetInfoLogger().LogWithContext(context, format, v...)
}
// InfoStartup logs application startup information using the global info logger
func InfoStartup(component string, message string) {
GetInfoLogger().LogStartup(component, message)
}
// InfoShutdown logs application shutdown information using the global info logger
func InfoShutdown(component string, message string) {
GetInfoLogger().LogShutdown(component, message)
}
// InfoOperation logs general operation information using the global info logger
func InfoOperation(operation string, details string) {
GetInfoLogger().LogOperation(operation, details)
}
// InfoStatus logs status changes or updates using the global info logger
func InfoStatus(component string, status string) {
GetInfoLogger().LogStatus(component, status)
}
// InfoRequest logs incoming requests using the global info logger
func InfoRequest(method string, path string, userAgent string) {
GetInfoLogger().LogRequest(method, path, userAgent)
}
// InfoResponse logs outgoing responses using the global info logger
func InfoResponse(method string, path string, statusCode int, duration string) {
GetInfoLogger().LogResponse(method, path, statusCode, duration)
}

View File

@@ -6,12 +6,10 @@ import (
)
var (
// Legacy logger for backward compatibility
logger *Logger
once sync.Once
)
// Logger maintains backward compatibility with existing code
type Logger struct {
base *BaseLogger
errorLogger *ErrorLogger
@@ -20,8 +18,6 @@ type Logger struct {
debugLogger *DebugLogger
}
// Initialize creates or gets the singleton logger instance
// This maintains backward compatibility with existing code
func Initialize() (*Logger, error) {
var err error
once.Do(func() {
@@ -31,13 +27,11 @@ func Initialize() (*Logger, error) {
}
func newLogger() (*Logger, error) {
// Initialize the base logger
baseLogger, err := InitializeBase("log")
if err != nil {
return nil, err
}
// Create the legacy logger wrapper
logger := &Logger{
base: baseLogger,
errorLogger: GetErrorLogger(),
@@ -49,7 +43,6 @@ func newLogger() (*Logger, error) {
return logger, nil
}
// Close closes the logger
func (l *Logger) Close() error {
if l.base != nil {
return l.base.Close()
@@ -57,7 +50,6 @@ func (l *Logger) Close() error {
return nil
}
// Legacy methods for backward compatibility
func (l *Logger) log(level, format string, v ...interface{}) {
if l.base != nil {
l.base.LogWithCaller(LogLevel(level), 3, format, v...)
@@ -94,13 +86,10 @@ func (l *Logger) Panic(format string) {
}
}
// Global convenience functions for backward compatibility
// These are now implemented in individual logger files to avoid redeclaration
func LegacyInfo(format string, v ...interface{}) {
if logger != nil {
logger.Info(format, v...)
} else {
// Fallback to direct logger if legacy logger not initialized
GetInfoLogger().Log(format, v...)
}
}
@@ -109,7 +98,6 @@ func LegacyError(format string, v ...interface{}) {
if logger != nil {
logger.Error(format, v...)
} else {
// Fallback to direct logger if legacy logger not initialized
GetErrorLogger().Log(format, v...)
}
}
@@ -118,7 +106,6 @@ func LegacyWarn(format string, v ...interface{}) {
if logger != nil {
logger.Warn(format, v...)
} else {
// Fallback to direct logger if legacy logger not initialized
GetWarnLogger().Log(format, v...)
}
}
@@ -127,7 +114,6 @@ func LegacyDebug(format string, v ...interface{}) {
if logger != nil {
logger.Debug(format, v...)
} else {
// Fallback to direct logger if legacy logger not initialized
GetDebugLogger().Log(format, v...)
}
}
@@ -136,55 +122,42 @@ func Panic(format string) {
if logger != nil {
logger.Panic(format)
} else {
// Fallback to direct logger if legacy logger not initialized
GetErrorLogger().LogFatal(format)
}
}
// Enhanced logging convenience functions
// These provide direct access to specialized logging functions
// LogStartup logs application startup information
func LogStartup(component string, message string) {
GetInfoLogger().LogStartup(component, message)
}
// LogShutdown logs application shutdown information
func LogShutdown(component string, message string) {
GetInfoLogger().LogShutdown(component, message)
}
// LogOperation logs general operation information
func LogOperation(operation string, details string) {
GetInfoLogger().LogOperation(operation, details)
}
// LogRequest logs incoming HTTP requests
func LogRequest(method string, path string, userAgent string) {
GetInfoLogger().LogRequest(method, path, userAgent)
}
// LogResponse logs outgoing HTTP responses
func LogResponse(method string, path string, statusCode int, duration string) {
GetInfoLogger().LogResponse(method, path, statusCode, duration)
}
// LogSQL logs SQL queries for debugging
func LogSQL(query string, args ...interface{}) {
GetDebugLogger().LogSQL(query, args...)
}
// LogMemory logs memory usage information
func LogMemory() {
GetDebugLogger().LogMemory()
}
// LogTiming logs timing information for performance debugging
func LogTiming(operation string, duration interface{}) {
GetDebugLogger().LogTiming(operation, duration)
}
// GetLegacyLogger returns the legacy logger instance for backward compatibility
func GetLegacyLogger() *Logger {
if logger == nil {
logger, _ = Initialize()
@@ -192,21 +165,17 @@ func GetLegacyLogger() *Logger {
return logger
}
// InitializeLogging initializes all logging components
func InitializeLogging() error {
// Initialize legacy logger for backward compatibility
_, err := Initialize()
if err != nil {
return fmt.Errorf("failed to initialize legacy logger: %v", err)
}
// Pre-initialize all logger types to ensure separate log files
GetErrorLogger()
GetWarnLogger()
GetInfoLogger()
GetDebugLogger()
// Log successful initialization
Info("Logging system initialized successfully")
return nil

View File

@@ -5,12 +5,10 @@ import (
"sync"
)
// WarnLogger handles warn-level logging
type WarnLogger struct {
base *BaseLogger
}
// NewWarnLogger creates a new warn logger instance
func NewWarnLogger() *WarnLogger {
base, _ := InitializeBase("warn")
return &WarnLogger{
@@ -18,14 +16,12 @@ func NewWarnLogger() *WarnLogger {
}
}
// Log writes a warn-level log entry
func (wl *WarnLogger) Log(format string, v ...interface{}) {
if wl.base != nil {
wl.base.Log(LogLevelWarn, format, v...)
}
}
// LogWithContext writes a warn-level log entry with additional context
func (wl *WarnLogger) LogWithContext(context string, format string, v ...interface{}) {
if wl.base != nil {
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
@@ -33,7 +29,6 @@ func (wl *WarnLogger) LogWithContext(context string, format string, v ...interfa
}
}
// LogDeprecation logs a deprecation warning
func (wl *WarnLogger) LogDeprecation(feature string, alternative string) {
if wl.base != nil {
if alternative != "" {
@@ -44,27 +39,23 @@ func (wl *WarnLogger) LogDeprecation(feature string, alternative string) {
}
}
// LogConfiguration logs configuration-related warnings
func (wl *WarnLogger) LogConfiguration(setting string, message string) {
if wl.base != nil {
wl.base.Log(LogLevelWarn, "CONFIG WARNING [%s]: %s", setting, message)
}
}
// LogPerformance logs performance-related warnings
func (wl *WarnLogger) LogPerformance(operation string, threshold string, actual string) {
if wl.base != nil {
wl.base.Log(LogLevelWarn, "PERFORMANCE WARNING [%s]: exceeded threshold %s, actual: %s", operation, threshold, actual)
}
}
// Global warn logger instance
var (
warnLogger *WarnLogger
warnOnce sync.Once
)
// GetWarnLogger returns the global warn logger instance
func GetWarnLogger() *WarnLogger {
warnOnce.Do(func() {
warnLogger = NewWarnLogger()
@@ -72,27 +63,22 @@ func GetWarnLogger() *WarnLogger {
return warnLogger
}
// Warn logs a warn-level message using the global warn logger
func Warn(format string, v ...interface{}) {
GetWarnLogger().Log(format, v...)
}
// WarnWithContext logs a warn-level message with context using the global warn logger
func WarnWithContext(context string, format string, v ...interface{}) {
GetWarnLogger().LogWithContext(context, format, v...)
}
// WarnDeprecation logs a deprecation warning using the global warn logger
func WarnDeprecation(feature string, alternative string) {
GetWarnLogger().LogDeprecation(feature, alternative)
}
// WarnConfiguration logs configuration-related warnings using the global warn logger
func WarnConfiguration(setting string, message string) {
GetWarnLogger().LogConfiguration(setting, message)
}
// WarnPerformance logs performance-related warnings using the global warn logger
func WarnPerformance(operation string, threshold string, actual string) {
GetWarnLogger().LogPerformance(operation, threshold, actual)
}

View File

@@ -6,12 +6,10 @@ import (
"time"
)
// IsPortAvailable checks if a port is available for both TCP and UDP
func IsPortAvailable(port int) bool {
return IsTCPPortAvailable(port) && IsUDPPortAvailable(port)
}
// IsTCPPortAvailable checks if a TCP port is available
func IsTCPPortAvailable(port int) bool {
addr := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", addr)
@@ -22,7 +20,6 @@ func IsTCPPortAvailable(port int) bool {
return true
}
// IsUDPPortAvailable checks if a UDP port is available
func IsUDPPortAvailable(port int) bool {
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
if err != nil {
@@ -32,7 +29,6 @@ func IsUDPPortAvailable(port int) bool {
return true
}
// FindAvailablePort finds an available port starting from the given port
func FindAvailablePort(startPort int) (int, error) {
maxPort := 65535
for port := startPort; port <= maxPort; port++ {
@@ -43,14 +39,12 @@ func FindAvailablePort(startPort int) (int, error) {
return 0, fmt.Errorf("no available ports found between %d and %d", startPort, maxPort)
}
// FindAvailablePortRange finds a range of consecutive available ports
func FindAvailablePortRange(startPort, count int) ([]int, error) {
maxPort := 65535
ports := make([]int, 0, count)
currentPort := startPort
for len(ports) < count && currentPort <= maxPort {
// Check if we have enough consecutive ports available
available := true
for i := 0; i < count-len(ports); i++ {
if !IsPortAvailable(currentPort + i) {
@@ -74,7 +68,6 @@ func FindAvailablePortRange(startPort, count int) ([]int, error) {
return ports, nil
}
// WaitForPortAvailable waits for a port to become available with timeout
func WaitForPortAvailable(port int, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
@@ -84,4 +77,4 @@ func WaitForPortAvailable(port int, timeout time.Duration) error {
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for port %d to become available", port)
}
}

View File

@@ -8,13 +8,10 @@ import (
)
const (
// MinPasswordLength defines the minimum password length
MinPasswordLength = 8
// BcryptCost defines the cost factor for bcrypt hashing
BcryptCost = 12
BcryptCost = 12
)
// HashPassword hashes a plain text password using bcrypt
func HashPassword(password string) (string, error) {
if len(password) < MinPasswordLength {
return "", errors.New("password must be at least 8 characters long")
@@ -28,12 +25,10 @@ func HashPassword(password string) (string, error) {
return string(hashedBytes), nil
}
// VerifyPassword verifies a plain text password against a hashed password
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// ValidatePasswordStrength validates password complexity requirements
func ValidatePasswordStrength(password string) error {
if len(password) < MinPasswordLength {
return errors.New("password must be at least 8 characters long")

View File

@@ -17,25 +17,23 @@ import (
func Start(di *dig.Container) *fiber.App {
app := fiber.New(fiber.Config{
EnablePrintRoutes: true,
ReadTimeout: 20 * time.Minute, // Increased for long-running Steam operations
WriteTimeout: 20 * time.Minute, // Increased for long-running Steam operations
IdleTimeout: 25 * time.Minute, // Increased accordingly
BodyLimit: 10 * 1024 * 1024, // 10MB
ReadTimeout: 20 * time.Minute,
WriteTimeout: 20 * time.Minute,
IdleTimeout: 25 * time.Minute,
BodyLimit: 10 * 1024 * 1024,
})
// Initialize security middleware
securityMW := security.NewSecurityMiddleware()
// Add security middleware stack
app.Use(securityMW.SecurityHeaders())
app.Use(securityMW.LogSecurityEvents())
app.Use(securityMW.TimeoutMiddleware(20 * time.Minute)) // Increased for Steam operations
app.Use(securityMW.RequestContextTimeout(20 * time.Minute)) // Increased for Steam operations
app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024)) // 10MB
app.Use(securityMW.TimeoutMiddleware(20 * time.Minute))
app.Use(securityMW.RequestContextTimeout(20 * time.Minute))
app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024))
app.Use(securityMW.ValidateUserAgent())
app.Use(securityMW.ValidateContentType("application/json", "application/x-www-form-urlencoded", "multipart/form-data"))
app.Use(securityMW.InputSanitization())
app.Use(securityMW.RateLimit(100, 1*time.Minute)) // 100 requests per minute global
app.Use(securityMW.RateLimit(100, 1*time.Minute))
app.Use(helmet.New())
@@ -62,7 +60,7 @@ func Start(di *dig.Container) *fiber.App {
port := os.Getenv("PORT")
if port == "" {
port = "3000" // Default port
port = "3000"
}
logging.Info("Starting server on port %s", port)

View File

@@ -7,11 +7,11 @@ import (
)
type LogTailer struct {
filePath string
handleLine func(string)
stopChan chan struct{}
isRunning bool
tracker *PositionTracker
filePath string
handleLine func(string)
stopChan chan struct{}
isRunning bool
tracker *PositionTracker
}
func NewLogTailer(filePath string, handleLine func(string)) *LogTailer {
@@ -30,10 +30,9 @@ func (t *LogTailer) Start() {
t.isRunning = true
go func() {
// Load last position from tracker
pos, err := t.tracker.LoadPosition()
if err != nil {
pos = &LogPosition{} // Start from beginning if error
pos = &LogPosition{}
}
lastSize := pos.LastPosition
@@ -43,7 +42,6 @@ func (t *LogTailer) Start() {
t.isRunning = false
return
default:
// Try to open and read the file
if file, err := os.Open(t.filePath); err == nil {
stat, err := file.Stat()
if err != nil {
@@ -52,12 +50,10 @@ func (t *LogTailer) Start() {
continue
}
// If file was truncated, start from beginning
if stat.Size() < lastSize {
lastSize = 0
}
// Seek to last read position
if lastSize > 0 {
file.Seek(lastSize, 0)
}
@@ -66,9 +62,8 @@ func (t *LogTailer) Start() {
for scanner.Scan() {
line := scanner.Text()
t.handleLine(line)
lastSize, _ = file.Seek(0, 1) // Get current position
// Save position periodically
lastSize, _ = file.Seek(0, 1)
t.tracker.SavePosition(&LogPosition{
LastPosition: lastSize,
LastRead: line,
@@ -78,7 +73,6 @@ func (t *LogTailer) Start() {
file.Close()
}
// Wait before next attempt
time.Sleep(time.Second)
}
}
@@ -90,4 +84,4 @@ func (t *LogTailer) Stop() {
return
}
close(t.stopChan)
}
}

View File

@@ -7,8 +7,8 @@ import (
)
type LogPosition struct {
LastPosition int64 `json:"last_position"`
LastRead string `json:"last_read"`
LastPosition int64 `json:"last_position"`
LastRead string `json:"last_read"`
}
type PositionTracker struct {
@@ -16,11 +16,10 @@ type PositionTracker struct {
}
func NewPositionTracker(logPath string) *PositionTracker {
// Create position file in same directory as log file
dir := filepath.Dir(logPath)
base := filepath.Base(logPath)
positionFile := filepath.Join(dir, "."+base+".position")
return &PositionTracker{
positionFile: positionFile,
}
@@ -30,7 +29,6 @@ func (t *PositionTracker) LoadPosition() (*LogPosition, error) {
data, err := os.ReadFile(t.positionFile)
if err != nil {
if os.IsNotExist(err) {
// Return empty position if file doesn't exist
return &LogPosition{}, nil
}
return nil, err
@@ -51,4 +49,4 @@ func (t *PositionTracker) SavePosition(pos *LogPosition) error {
}
return os.WriteFile(t.positionFile, data, 0644)
}
}

View File

@@ -80,7 +80,7 @@ func TailLogFile(path string, callback func(string)) {
file, _ := os.Open(path)
defer file.Close()
file.Seek(0, os.SEEK_END) // Start at end of file
file.Seek(0, os.SEEK_END)
reader := bufio.NewReader(file)
for {
@@ -88,7 +88,7 @@ func TailLogFile(path string, callback func(string)) {
if err == nil {
callback(line)
} else {
time.Sleep(500 * time.Millisecond) // wait for new data
time.Sleep(500 * time.Millisecond)
}
}
}

View File

@@ -0,0 +1,169 @@
package websocket
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/logging"
"encoding/json"
"sync"
"time"
"github.com/gofiber/websocket/v2"
"github.com/google/uuid"
)
type WebSocketConnection struct {
conn *websocket.Conn
serverID *uuid.UUID
userID *uuid.UUID
}
type WebSocketService struct {
connections sync.Map
mu sync.RWMutex
}
func NewWebSocketService() *WebSocketService {
return &WebSocketService{}
}
func (ws *WebSocketService) AddConnection(connID string, conn *websocket.Conn, userID *uuid.UUID) {
wsConn := &WebSocketConnection{
conn: conn,
userID: userID,
}
ws.connections.Store(connID, wsConn)
logging.Info("WebSocket connection added: %s for user: %v", connID, userID)
}
func (ws *WebSocketService) RemoveConnection(connID string) {
if conn, exists := ws.connections.LoadAndDelete(connID); exists {
if wsConn, ok := conn.(*WebSocketConnection); ok {
wsConn.conn.Close()
}
}
logging.Info("WebSocket connection removed: %s", connID)
}
func (ws *WebSocketService) SetServerID(connID string, serverID uuid.UUID) {
if conn, exists := ws.connections.Load(connID); exists {
if wsConn, ok := conn.(*WebSocketConnection); ok {
wsConn.serverID = &serverID
}
}
}
func (ws *WebSocketService) BroadcastStep(serverID uuid.UUID, step model.ServerCreationStep, status model.StepStatus, message string, errorMsg string) {
stepMsg := model.StepMessage{
Step: step,
Status: status,
Message: message,
Error: errorMsg,
}
wsMsg := model.WebSocketMessage{
Type: model.MessageTypeStep,
ServerID: &serverID,
Timestamp: time.Now().Unix(),
Data: stepMsg,
}
ws.broadcastToServer(serverID, wsMsg)
}
func (ws *WebSocketService) BroadcastSteamOutput(serverID uuid.UUID, output string, isError bool) {
steamMsg := model.SteamOutputMessage{
Output: output,
IsError: isError,
}
wsMsg := model.WebSocketMessage{
Type: model.MessageTypeSteamOutput,
ServerID: &serverID,
Timestamp: time.Now().Unix(),
Data: steamMsg,
}
ws.broadcastToServer(serverID, wsMsg)
}
func (ws *WebSocketService) BroadcastError(serverID uuid.UUID, error string, details string) {
errorMsg := model.ErrorMessage{
Error: error,
Details: details,
}
wsMsg := model.WebSocketMessage{
Type: model.MessageTypeError,
ServerID: &serverID,
Timestamp: time.Now().Unix(),
Data: errorMsg,
}
ws.broadcastToServer(serverID, wsMsg)
}
func (ws *WebSocketService) BroadcastComplete(serverID uuid.UUID, success bool, message string) {
completeMsg := model.CompleteMessage{
ServerID: serverID,
Success: success,
Message: message,
}
wsMsg := model.WebSocketMessage{
Type: model.MessageTypeComplete,
ServerID: &serverID,
Timestamp: time.Now().Unix(),
Data: completeMsg,
}
ws.broadcastToServer(serverID, wsMsg)
}
func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.WebSocketMessage) {
data, err := json.Marshal(message)
if err != nil {
logging.Error("Failed to marshal WebSocket message: %v", err)
return
}
ws.connections.Range(func(key, value interface{}) bool {
if wsConn, ok := value.(*WebSocketConnection); ok {
if wsConn.serverID != nil && *wsConn.serverID == serverID {
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
ws.RemoveConnection(key.(string))
}
}
}
return true
})
}
func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebSocketMessage) {
data, err := json.Marshal(message)
if err != nil {
logging.Error("Failed to marshal WebSocket message: %v", err)
return
}
ws.connections.Range(func(key, value interface{}) bool {
if wsConn, ok := value.(*WebSocketConnection); ok {
if wsConn.userID != nil && *wsConn.userID == userID {
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
ws.RemoveConnection(key.(string))
}
}
}
return true
})
}
func (ws *WebSocketService) GetActiveConnections() int {
count := 0
ws.connections.Range(func(key, value interface{}) bool {
count++
return true
})
return count
}