package service import ( "context" "encoding/json" "fmt" "log" "time" "equipment-analyzer/internal/capture" "equipment-analyzer/internal/config" "equipment-analyzer/internal/model" "equipment-analyzer/internal/utils" "github.com/wailsapp/wails/v2/pkg/runtime" ) type App struct { ctx context.Context config *config.Config logger *utils.Logger captureService *CaptureService parserService *ParserService database *model.Database databaseService *DatabaseService } func NewApp(cfg *config.Config, logger *utils.Logger) *App { // init database database, err := model.NewDatabase() if err != nil { logger.Error("database init failed", "error", err) // allow app to run without db features parserService := NewParserService(cfg, logger) return &App{ config: cfg, logger: logger, captureService: NewCaptureService(cfg, logger, parserService), parserService: parserService, } } databaseService := NewDatabaseService(database, logger) parserService := NewParserService(cfg, logger) return &App{ config: cfg, logger: logger, captureService: NewCaptureService(cfg, logger, parserService), parserService: parserService, database: database, databaseService: databaseService, } } func (a *App) Startup(ctx context.Context) { a.ctx = ctx a.captureService.SetBeforeRemote(func() { if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:ready_to_parse") } }) a.logger.Info("app startup") } func (a *App) DomReady(ctx context.Context) { a.logger.Info("dom ready") } func (a *App) BeforeClose(ctx context.Context) (prevent bool) { a.logger.Info("app closing") return false } func (a *App) Shutdown(ctx context.Context) { a.logger.Info("app shutdown") if a.database != nil { if err := a.database.Close(); err != nil { a.logger.Error("failed to close database connection", "error", err) } else { a.logger.Info("database connection closed") } } } // GetNetworkInterfaces returns available network interfaces. func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) { interfaces, err := capture.GetNetworkInterfaces() if err != nil { a.logger.Error("get network interfaces failed", "error", err) return nil, err } return interfaces, nil } // StartCapture starts capture on the given interface. func (a *App) StartCapture(interfaceName string) error { if a.captureService.IsCapturing() { return fmt.Errorf("capture already running") } timeoutMs := a.config.Capture.DefaultTimeout if timeoutMs > 500 { log.Printf("[service] capture timeout too high (%dms), clamp to 500ms for responsive stop", timeoutMs) timeoutMs = 500 } config := capture.Config{ InterfaceName: interfaceName, Filter: a.config.Capture.DefaultFilter, Timeout: time.Duration(timeoutMs) * time.Millisecond, BufferSize: a.config.Capture.BufferSize, } err := a.captureService.StartCaptureAsync(context.Background(), config, func() { if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:started") } }, func(err error) { a.logger.Error("start capture failed", "error", err) if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:start_failed", err.Error()) } }) if err != nil { return err } a.logger.Info("capture start requested", "interface", interfaceName) return nil } // StopCapture stops capture. func (a *App) StopCapture() error { if !a.captureService.IsCapturing() { return fmt.Errorf("capture not running") } err := a.captureService.StopCapture() if err != nil { a.logger.Error("stop capture failed", "error", err) return err } a.captureService.ProcessAllData() a.logger.Info("capture stopped") return nil } // GetCapturedData returns raw captured data. func (a *App) GetCapturedData() ([]string, error) { return a.captureService.GetCapturedData(), nil } // ParseData parses captured data to JSON (remote parser). func (a *App) ParseData(hexDataList []string) (string, error) { _, rawJson, err := a.parserService.ParseHexData(hexDataList) if err != nil { a.logger.Error("parse data failed", "error", err) return "", err } return rawJson, nil } // ExportData exports data to a file. func (a *App) ExportData(hexDataList []string, filename string) error { result, rawJson, err := a.parserService.ParseHexData(hexDataList) if err != nil { a.logger.Error("parse data failed", "error", err) return err } a.logger.Info("export data", "filename", filename, "count", len(result.Items)) err = utils.WriteFile(filename, []byte(rawJson)) if err != nil { a.logger.Error("write file failed", "error", err) return err } return nil } // ExportCurrentData exports latest data from database to a file. func (a *App) ExportCurrentData(filename string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } parsedResult, err := a.GetLatestParsedDataFromDatabase() if err != nil { a.logger.Error("failed to load data from database", "error", err) return err } if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) { return fmt.Errorf("no data to export") } exportData := map[string]interface{}{ "items": parsedResult.Items, "heroes": parsedResult.Heroes, } jsonData, err := json.MarshalIndent(exportData, "", " ") if err != nil { a.logger.Error("failed to marshal data", "error", err) return err } err = utils.WriteFile(filename, jsonData) if err != nil { a.logger.Error("write file failed", "error", err) return err } a.logger.Info("export current data succeeded", "filename", filename, "items_count", len(parsedResult.Items), "heroes_count", len(parsedResult.Heroes)) return nil } // GetCurrentDataForExport returns latest data from database as JSON string. func (a *App) GetCurrentDataForExport() (string, error) { if a.databaseService == nil { return "", fmt.Errorf("database service not initialized") } parsedResult, err := a.GetLatestParsedDataFromDatabase() if err != nil { a.logger.Error("failed to load data from database", "error", err) return "", err } if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) { return "", fmt.Errorf("no data to export") } exportData := map[string]interface{}{ "items": parsedResult.Items, "heroes": parsedResult.Heroes, } jsonData, err := json.MarshalIndent(exportData, "", " ") if err != nil { a.logger.Error("failed to marshal data", "error", err) return "", err } return string(jsonData), nil } // GetCaptureStatus returns capture status. func (a *App) GetCaptureStatus() model.CaptureStatus { return model.CaptureStatus{ IsCapturing: a.captureService.IsCapturing(), Status: a.getStatusMessage(), } } func (a *App) getStatusMessage() string { if a.captureService.IsCapturing() { return "capturing" } if a.captureService.IsStarting() { return "starting" } return "ready" } // ReadRawJsonFile is deprecated; use GetLatestParsedDataFromDatabase. func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) { return a.GetLatestParsedDataFromDatabase() } // StopAndParseCapture stops capture and parses data. func (a *App) StopAndParseCapture() (*model.ParsedResult, error) { log.Printf("[service] StopAndParseCapture entry") result, err := a.captureService.StopAndParseCapture() if err != nil { a.logger.Error("stop and parse capture failed", "error", err) return nil, err } if a.databaseService != nil && result != nil { itemsJSON := "[]" if result.Items != nil { if jsonData, err := json.Marshal(result.Items); err == nil { itemsJSON = string(jsonData) } } heroesJSON := "[]" if result.Heroes != nil { if jsonData, err := json.Marshal(result.Heroes); err == nil { heroesJSON = string(jsonData) } } sessionName := fmt.Sprintf("capture_%d", time.Now().Unix()) if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil { a.logger.Error("save parsed data failed", "error", err) } else { a.logger.Info("parsed data saved", "session_name", sessionName) } } return result, nil } // SaveParsedDataToDatabase saves parsed data. func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON) } // GetLatestParsedDataFromDatabase returns latest parsed data from database. func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase() if err != nil { return nil, err } var items []interface{} if itemsJSON != "" { if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil { return nil, fmt.Errorf("failed to unmarshal items: %w", err) } } var heroes []interface{} if heroesJSON != "" { if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil { return nil, fmt.Errorf("failed to unmarshal heroes: %w", err) } } return &model.ParsedResult{ Items: items, Heroes: heroes, }, nil } // GetParsedSessions returns all parsed sessions. func (a *App) GetParsedSessions() ([]model.ParsedSession, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } return a.databaseService.GetParsedSessions() } // GetParsedDataByID returns parsed data by session id. func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } itemsJSON, heroesJSON, err := a.databaseService.GetParsedDataByID(id) if err != nil { return nil, err } var items []interface{} if itemsJSON != "" { if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil { return nil, fmt.Errorf("failed to unmarshal items: %w", err) } } var heroes []interface{} if heroesJSON != "" { if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil { return nil, fmt.Errorf("failed to unmarshal heroes: %w", err) } } return &model.ParsedResult{ Items: items, Heroes: heroes, }, nil } // UpdateParsedSessionName updates session name. func (a *App) UpdateParsedSessionName(id int64, name string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } if name == "" { return fmt.Errorf("session name cannot be empty") } return a.databaseService.UpdateParsedSessionName(id, name) } // DeleteParsedSession deletes a parsed session by id. func (a *App) DeleteParsedSession(id int64) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } return a.databaseService.DeleteParsedSession(id) } // SaveAppSetting saves app setting. func (a *App) SaveAppSetting(key, value string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } return a.databaseService.SaveAppSetting(key, value) } // GetAppSetting gets app setting. func (a *App) GetAppSetting(key string) (string, error) { if a.databaseService == nil { return "", fmt.Errorf("database service not initialized") } return a.databaseService.GetAppSetting(key) } // GetAllAppSettings gets all app settings. func (a *App) GetAllAppSettings() (map[string]string, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } return a.databaseService.GetAllAppSettings() } // StartCaptureWithFilter allows frontend to provide a custom BPF filter. func (a *App) StartCaptureWithFilter(interfaceName string, filter string) error { a.logger.Info("StartCaptureWithFilter requested", "interface", interfaceName, "filter", filter) if a.captureService.IsCapturing() { return fmt.Errorf("capture already running") } useFilter := filter if useFilter == "" { useFilter = a.config.Capture.DefaultFilter } timeoutMs := a.config.Capture.DefaultTimeout if timeoutMs > 500 { log.Printf("[service] capture timeout too high (%dms), clamp to 500ms for responsive stop", timeoutMs) timeoutMs = 500 } config := capture.Config{ InterfaceName: interfaceName, Filter: useFilter, Timeout: time.Duration(timeoutMs) * time.Millisecond, BufferSize: a.config.Capture.BufferSize, } err := a.captureService.StartCaptureAsync(context.Background(), config, func() { if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:started") } }, func(err error) { a.logger.Error("start capture failed", "error", err) if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:start_failed", err.Error()) } }) if err != nil { return err } a.logger.Info("capture start requested", "interface", interfaceName, "filter", useFilter) return nil }