@@ -5,11 +5,13 @@ import (
5
5
"context"
6
6
"fmt"
7
7
"io"
8
+ "log"
8
9
"os"
9
10
"os/exec"
10
11
"path/filepath"
11
12
"strings"
12
13
"sync"
14
+ "syscall"
13
15
"time"
14
16
15
17
"fyne.io/fyne/v2"
@@ -21,6 +23,7 @@ type Config struct {
21
23
BackendsPath string `json:"backends_path"`
22
24
Address string `json:"address"`
23
25
AutoStart bool `json:"auto_start"`
26
+ StartOnBoot bool `json:"start_on_boot"`
24
27
LogLevel string `json:"log_level"`
25
28
EnvironmentVars map [string ]string `json:"environment_vars"`
26
29
}
@@ -42,6 +45,10 @@ type Launcher struct {
42
45
logMutex sync.RWMutex
43
46
statusChannel chan string
44
47
48
+ // Logging
49
+ logFile * os.File
50
+ logPath string
51
+
45
52
// UI state
46
53
lastUpdateCheck time.Time
47
54
}
@@ -57,8 +64,35 @@ func NewLauncher() *Launcher {
57
64
}
58
65
}
59
66
67
+ // setupLogging sets up log file for LocalAI process output
68
+ func (l * Launcher ) setupLogging () error {
69
+ // Create logs directory in data folder
70
+ dataPath := l .GetDataPath ()
71
+ logsDir := filepath .Join (dataPath , "logs" )
72
+ if err := os .MkdirAll (logsDir , 0755 ); err != nil {
73
+ return fmt .Errorf ("failed to create logs directory: %w" , err )
74
+ }
75
+
76
+ // Create log file with timestamp
77
+ timestamp := time .Now ().Format ("2006-01-02_15-04-05" )
78
+ l .logPath = filepath .Join (logsDir , fmt .Sprintf ("localai_%s.log" , timestamp ))
79
+
80
+ logFile , err := os .Create (l .logPath )
81
+ if err != nil {
82
+ return fmt .Errorf ("failed to create log file: %w" , err )
83
+ }
84
+
85
+ l .logFile = logFile
86
+ return nil
87
+ }
88
+
60
89
// Initialize sets up the launcher
61
90
func (l * Launcher ) Initialize () error {
91
+ // Setup logging
92
+ if err := l .setupLogging (); err != nil {
93
+ return fmt .Errorf ("failed to setup logging: %w" , err )
94
+ }
95
+
62
96
// Load configuration
63
97
if err := l .loadConfig (); err != nil {
64
98
return fmt .Errorf ("failed to load config: %w" , err )
@@ -98,7 +132,12 @@ func (l *Launcher) Initialize() error {
98
132
time .Sleep (1 * time .Second ) // Wait for UI to be ready
99
133
available , version , err := l .CheckForUpdates ()
100
134
if err == nil && available {
101
- l .ui .NotifyUpdateAvailable (version )
135
+ if l .systray != nil {
136
+ l .systray .NotifyUpdateAvailable (version )
137
+ }
138
+ if l .ui != nil {
139
+ l .ui .NotifyUpdateAvailable (version )
140
+ }
102
141
}
103
142
}()
104
143
}
@@ -165,8 +204,9 @@ func (l *Launcher) StartLocalAI() error {
165
204
go l .monitorLogs (stdout , "STDOUT" )
166
205
go l .monitorLogs (stderr , "STDERR" )
167
206
168
- // Monitor process
207
+ // Monitor process with startup timeout
169
208
go func () {
209
+ // Wait for process to start or fail
170
210
err := l .localaiCmd .Wait ()
171
211
l .isRunning = false
172
212
l .updateRunningState (false )
@@ -177,6 +217,22 @@ func (l *Launcher) StartLocalAI() error {
177
217
}
178
218
}()
179
219
220
+ // Add startup timeout detection
221
+ go func () {
222
+ time .Sleep (10 * time .Second ) // Wait 10 seconds for startup
223
+ if l .isRunning {
224
+ // Check if process is still alive
225
+ if l .localaiCmd .Process != nil {
226
+ if err := l .localaiCmd .Process .Signal (syscall .Signal (0 )); err != nil {
227
+ // Process is dead, mark as not running
228
+ l .isRunning = false
229
+ l .updateRunningState (false )
230
+ l .updateStatus ("LocalAI failed to start properly" )
231
+ }
232
+ }
233
+ }
234
+ }()
235
+
180
236
return nil
181
237
}
182
238
@@ -212,6 +268,22 @@ func (l *Launcher) GetLogs() string {
212
268
return l .logBuffer .String ()
213
269
}
214
270
271
+ // GetRecentLogs returns the most recent logs (last 50 lines) for better error display
272
+ func (l * Launcher ) GetRecentLogs () string {
273
+ l .logMutex .RLock ()
274
+ defer l .logMutex .RUnlock ()
275
+
276
+ content := l .logBuffer .String ()
277
+ lines := strings .Split (content , "\n " )
278
+
279
+ // Get last 50 lines
280
+ if len (lines ) > 50 {
281
+ lines = lines [len (lines )- 50 :]
282
+ }
283
+
284
+ return strings .Join (lines , "\n " )
285
+ }
286
+
215
287
// GetConfig returns the current configuration
216
288
func (l * Launcher ) GetConfig () * Config {
217
289
return l .config
@@ -235,6 +307,23 @@ func (l *Launcher) GetWebUIURL() string {
235
307
return address
236
308
}
237
309
310
+ // GetDataPath returns the path where LocalAI data and logs are stored
311
+ func (l * Launcher ) GetDataPath () string {
312
+ // LocalAI typically stores data in the current working directory or a models directory
313
+ // First check if models path is configured
314
+ if l .config != nil && l .config .ModelsPath != "" {
315
+ // Return the parent directory of models path
316
+ return filepath .Dir (l .config .ModelsPath )
317
+ }
318
+
319
+ // Fallback to home directory LocalAI folder
320
+ homeDir , err := os .UserHomeDir ()
321
+ if err != nil {
322
+ return "."
323
+ }
324
+ return filepath .Join (homeDir , ".localai" )
325
+ }
326
+
238
327
// CheckForUpdates checks if there are any available updates
239
328
func (l * Launcher ) CheckForUpdates () (bool , string , error ) {
240
329
available , version , err := l .releaseManager .IsUpdateAvailable ()
@@ -276,6 +365,13 @@ func (l *Launcher) monitorLogs(reader io.Reader, prefix string) {
276
365
}
277
366
l .logMutex .Unlock ()
278
367
368
+ // Write to log file if available
369
+ if l .logFile != nil {
370
+ if _ , err := l .logFile .WriteString (logLine ); err != nil {
371
+ log .Printf ("Failed to write to log file: %v" , err )
372
+ }
373
+ }
374
+
279
375
// Notify UI of new log content
280
376
if l .ui != nil {
281
377
l .ui .OnLogUpdate (logLine )
@@ -323,6 +419,9 @@ func (l *Launcher) periodicUpdateCheck() {
323
419
available , version , err := l .CheckForUpdates ()
324
420
if err == nil && available {
325
421
l .updateStatus (fmt .Sprintf ("Update available: %s" , version ))
422
+ if l .systray != nil {
423
+ l .systray .NotifyUpdateAvailable (version )
424
+ }
326
425
if l .ui != nil {
327
426
l .ui .NotifyUpdateAvailable (version )
328
427
}
0 commit comments