@@ -12,6 +12,7 @@ import (
1212 "path/filepath"
1313 "runtime"
1414 "strings"
15+ "sync"
1516 "time"
1617
1718 "surfmanager/internal/apps"
@@ -31,13 +32,55 @@ type Note struct {
3132 UpdatedAt string `json:"updated_at"`
3233}
3334
35+ // logLine writes to console, emits to frontend, and appends to log file
36+ func (a * App ) logLine (msg string ) {
37+ fmt .Println (msg )
38+ if a .ctx != nil {
39+ wailsRuntime .EventsEmit (a .ctx , "log" , msg )
40+ }
41+ if a .logFilePath == "" {
42+ return
43+ }
44+ a .logMutex .Lock ()
45+ defer a .logMutex .Unlock ()
46+ f , err := os .OpenFile (a .logFilePath , os .O_CREATE | os .O_APPEND | os .O_WRONLY , 0644 )
47+ if err != nil {
48+ return
49+ }
50+ defer f .Close ()
51+ timestamp := time .Now ().Format (time .RFC3339 )
52+ fmt .Fprintf (f , "%s %s\n " , timestamp , msg )
53+ }
54+
55+ // GetLogs returns the current activity log content
56+ func (a * App ) GetLogs () (string , error ) {
57+ if a .logFilePath == "" {
58+ return "" , fmt .Errorf ("log file not initialized" )
59+ }
60+ data , err := os .ReadFile (a .logFilePath )
61+ if err != nil {
62+ return "" , err
63+ }
64+ return string (data ), nil
65+ }
66+
67+ // LogMessage allows frontend to append a log line
68+ func (a * App ) LogMessage (message string ) {
69+ if message == "" {
70+ return
71+ }
72+ a .logLine (message )
73+ }
74+
3475// App struct holds all backend managers and provides Wails-bound methods.
3576type App struct {
36- ctx context.Context
37- config * config.Manager
38- process * process.Killer
39- backup * backup.Manager
40- apps * apps.ConfigLoader
77+ ctx context.Context
78+ config * config.Manager
79+ process * process.Killer
80+ backup * backup.Manager
81+ apps * apps.ConfigLoader
82+ logFilePath string
83+ logMutex sync.Mutex
4184}
4285
4386// NewApp creates a new App application struct.
@@ -54,17 +97,25 @@ func (a *App) startup(ctx context.Context) {
5497 fmt .Printf ("Warning: Failed to create SurfManager directories: %v\n " , err )
5598 }
5699
100+ // Prepare log file under Documents/SurfManager/logs/app.log
101+ logsDir := filepath .Join (a .config .GetDocumentsDir (), "SurfManager" , "logs" )
102+ os .MkdirAll (logsDir , 0755 )
103+ a .logFilePath = filepath .Join (logsDir , "app.log" )
104+ // Touch file
105+ if f , fileErr := os .OpenFile (a .logFilePath , os .O_CREATE | os .O_APPEND | os .O_WRONLY , 0644 ); fileErr == nil {
106+ f .Close ()
107+ }
108+
57109 a .process = process .NewKiller (func (msg string ) {
58- fmt .Println (msg )
59- wailsRuntime .EventsEmit (ctx , "log" , msg )
110+ a .logLine (msg )
60111 })
61112
62113 a .backup = backup .NewManager (a .config .GetDocumentsDir ())
63114
64- var err error
65- a .apps , err = apps .NewConfigLoader ()
66- if err != nil {
67- fmt .Printf ("Warning: Failed to initialize apps loader: %v\n " , err )
115+ var loaderErr error
116+ a .apps , loaderErr = apps .NewConfigLoader ()
117+ if loaderErr != nil {
118+ fmt .Printf ("Warning: Failed to initialize apps loader: %v\n " , loaderErr )
68119 } else {
69120 if err := a .apps .LoadAllConfigs (); err != nil {
70121 fmt .Printf ("Warning: Failed to load app configs: %v\n " , err )
@@ -190,11 +241,7 @@ func (a *App) ResetApp(appKey string, autoBackup bool, skipClose bool) error {
190241 return fmt .Errorf ("no data folder found for %s" , cfg .DisplayName )
191242 }
192243
193- // Get process names from exe paths
194- var processNames []string
195- for _ , exePath := range cfg .Paths .ExePaths {
196- processNames = append (processNames , filepath .Base (exePath ))
197- }
244+ processNames := a .collectProcessNames (cfg )
198245
199246 // Smart close the app (unless skipClose is true)
200247 if ! skipClose && len (processNames ) > 0 {
@@ -312,6 +359,12 @@ func (a *App) GenerateNewID(appKey string) (int, error) {
312359 return nil
313360 }
314361
362+ // Validate JSON size to prevent potential DoS attacks
363+ const maxJSONSize = 10 * 1024 * 1024 // 10MB limit
364+ if len (data ) > maxJSONSize {
365+ return nil
366+ }
367+
315368 var jsonData map [string ]interface {}
316369 if err := json .Unmarshal (data , & jsonData ); err != nil {
317370 return nil
@@ -422,10 +475,7 @@ func (a *App) KillApp(appKey string) error {
422475 return fmt .Errorf ("app not found: %s" , appKey )
423476 }
424477
425- var processNames []string
426- for _ , exePath := range cfg .Paths .ExePaths {
427- processNames = append (processNames , filepath .Base (exePath ))
428- }
478+ processNames := a .collectProcessNames (cfg )
429479
430480 if len (processNames ) == 0 {
431481 return nil
@@ -445,11 +495,7 @@ func (a *App) ResetAddonData(appKey string, skipClose bool) error {
445495 return fmt .Errorf ("no addon folders configured for %s" , cfg .DisplayName )
446496 }
447497
448- // Get process names from exe paths
449- var processNames []string
450- for _ , exePath := range cfg .Paths .ExePaths {
451- processNames = append (processNames , filepath .Base (exePath ))
452- }
498+ processNames := a .collectProcessNames (cfg )
453499
454500 // Smart close the app (unless skipClose is true)
455501 if ! skipClose && len (processNames ) > 0 {
@@ -492,18 +538,28 @@ func (a *App) ResetAddonData(appKey string, skipClose bool) error {
492538 return nil
493539}
494540
541+ func (a * App ) collectProcessNames (cfg * apps.AppConfig ) []string {
542+ var processNames []string
543+ for _ , exePath := range cfg .Paths .ExePaths {
544+ processNames = append (processNames , filepath .Base (exePath ))
545+ }
546+ if len (cfg .Paths .ProcessNames ) > 0 {
547+ processNames = append (processNames , cfg .Paths .ProcessNames ... )
548+ }
549+ return processNames
550+ }
551+
495552// IsAppRunning checks if an app is currently running
496553func (a * App ) IsAppRunning (appKey string ) bool {
497554 cfg := a .GetApp (appKey )
498555 if cfg == nil {
499556 return false
500557 }
501558
502- var processNames [] string
503- for _ , exePath := range cfg . Paths . ExePaths {
504- processNames = append ( processNames , filepath . Base ( exePath ))
559+ processNames := a . collectProcessNames ( cfg )
560+ if len ( processNames ) == 0 {
561+ return false
505562 }
506-
507563 return a .process .IsRunning (processNames )
508564}
509565
@@ -528,7 +584,7 @@ func (a *App) CalculateBackupSize(appKey string, includeData bool) (map[string]i
528584 // Calculate size for each backup item
529585 for _ , item := range cfg .BackupItems {
530586 itemPath := filepath .Join (dataPath , item .Path )
531-
587+
532588 // Check if path exists
533589 info , err := os .Stat (itemPath )
534590 if err != nil {
@@ -572,12 +628,12 @@ func (a *App) CalculateBackupSize(appKey string, includeData bool) (map[string]i
572628
573629 // Build result map
574630 result := map [string ]interface {}{
575- "total_size" : totalSize ,
576- "data_size" : dataSize ,
577- "addon_size" : addonSize ,
578- "total_size_formatted" : backup .FormatSize (totalSize ),
579- "data_size_formatted" : backup .FormatSize (dataSize ),
580- "addon_size_formatted" : backup .FormatSize (addonSize ),
631+ "total_size" : totalSize ,
632+ "data_size" : dataSize ,
633+ "addon_size" : addonSize ,
634+ "total_size_formatted" : backup .FormatSize (totalSize ),
635+ "data_size_formatted" : backup .FormatSize (dataSize ),
636+ "addon_size_formatted" : backup .FormatSize (addonSize ),
581637 }
582638
583639 return result , nil
@@ -647,10 +703,7 @@ func (a *App) CreateBackup(appKey, sessionName string, addonOnly bool) error {
647703 }
648704
649705 // Smart close the app first (unless addonOnly)
650- var processNames []string
651- for _ , exePath := range cfg .Paths .ExePaths {
652- processNames = append (processNames , filepath .Base (exePath ))
653- }
706+ processNames := a .collectProcessNames (cfg )
654707
655708 if ! addonOnly && len (processNames ) > 0 {
656709 wailsRuntime .EventsEmit (a .ctx , "progress" , map [string ]interface {}{
@@ -698,10 +751,7 @@ func (a *App) RestoreBackup(appKey, sessionName string, skipClose bool) error {
698751 }
699752
700753 // Smart close the app first (unless skipClose is true)
701- var processNames []string
702- for _ , exePath := range cfg .Paths .ExePaths {
703- processNames = append (processNames , filepath .Base (exePath ))
704- }
754+ processNames := a .collectProcessNames (cfg )
705755
706756 if ! skipClose && len (processNames ) > 0 {
707757 wailsRuntime .EventsEmit (a .ctx , "progress" , map [string ]interface {}{
@@ -751,10 +801,7 @@ func (a *App) RestoreAccountOnly(appKey, sessionName string) error {
751801 }
752802
753803 // Always close the app first (required for file lock release)
754- var processNames []string
755- for _ , exePath := range cfg .Paths .ExePaths {
756- processNames = append (processNames , filepath .Base (exePath ))
757- }
804+ processNames := a .collectProcessNames (cfg )
758805
759806 if len (processNames ) > 0 {
760807 wailsRuntime .EventsEmit (a .ctx , "progress" , map [string ]interface {}{
@@ -985,11 +1032,22 @@ func (a *App) GetNotes() ([]Note, error) {
9851032 continue
9861033 }
9871034
1035+ // Validate JSON size to prevent potential DoS attacks
1036+ const maxJSONSize = 1 * 1024 * 1024 // 1MB limit for notes
1037+ if len (data ) > maxJSONSize {
1038+ continue
1039+ }
1040+
9881041 var note Note
9891042 if err := json .Unmarshal (data , & note ); err != nil {
9901043 continue
9911044 }
9921045
1046+ // Validate note structure
1047+ if note .Title == "" || note .Content == "" {
1048+ continue
1049+ }
1050+
9931051 note .ID = strings .TrimSuffix (entry .Name (), ".json" )
9941052 notes = append (notes , note )
9951053 }
@@ -999,6 +1057,11 @@ func (a *App) GetNotes() ([]Note, error) {
9991057
10001058// GetNote returns a specific note
10011059func (a * App ) GetNote (id string ) (* Note , error ) {
1060+ // Validate ID to prevent path traversal
1061+ if strings .Contains (id , ".." ) || strings .Contains (id , "/" ) || strings .Contains (id , "\\ " ) {
1062+ return nil , fmt .Errorf ("invalid note ID" )
1063+ }
1064+
10021065 notesDir := a .config .GetNotesDir ()
10031066 filePath := filepath .Join (notesDir , id + ".json" )
10041067
@@ -1007,11 +1070,22 @@ func (a *App) GetNote(id string) (*Note, error) {
10071070 return nil , err
10081071 }
10091072
1073+ // Validate JSON size to prevent potential DoS attacks
1074+ const maxJSONSize = 1 * 1024 * 1024 // 1MB limit for notes
1075+ if len (data ) > maxJSONSize {
1076+ return nil , fmt .Errorf ("note file too large" )
1077+ }
1078+
10101079 var note Note
10111080 if err := json .Unmarshal (data , & note ); err != nil {
10121081 return nil , err
10131082 }
10141083
1084+ // Validate note structure
1085+ if note .Title == "" || note .Content == "" {
1086+ return nil , fmt .Errorf ("invalid note structure" )
1087+ }
1088+
10151089 note .ID = id
10161090 return & note , nil
10171091}
@@ -1224,11 +1298,7 @@ func (a *App) RestoreAddonOnly(appKey, sessionName string, skipClose bool) error
12241298 return fmt .Errorf ("session '%s' does not have addon backups" , sessionName )
12251299 }
12261300
1227- // Get process names from exe paths
1228- var processNames []string
1229- for _ , exePath := range cfg .Paths .ExePaths {
1230- processNames = append (processNames , filepath .Base (exePath ))
1231- }
1301+ processNames := a .collectProcessNames (cfg )
12321302
12331303 // Smart close the app (unless skipClose is true)
12341304 if ! skipClose && len (processNames ) > 0 {
0 commit comments