|
42 | 42 | defaultCoreURL = "http://127.0.0.1:8080" |
43 | 43 | errNoBundledCore = errors.New("no bundled core binary found") |
44 | 44 | trayAPIKey = "" // API key generated for core communication |
| 45 | + shutdownComplete = make(chan struct{}) // Signal when shutdown is complete |
45 | 46 | ) |
46 | 47 |
|
47 | 48 | // getLogDir returns the standard log directory for the current OS. |
@@ -152,21 +153,32 @@ func main() { |
152 | 153 |
|
153 | 154 | // Create tray application early so icon appears |
154 | 155 | shutdownFunc := func() { |
| 156 | + defer close(shutdownComplete) // Signal when shutdown is done |
| 157 | + |
155 | 158 | logger.Info("Tray shutdown requested") |
156 | | - // IMPORTANT: Send shutdown event to trigger state transition |
| 159 | + |
| 160 | + // Send shutdown event to state machine |
157 | 161 | stateMachine.SendEvent(state.EventShutdown) |
158 | | - // IMPORTANT: Call handleShutdown() directly to terminate core process |
159 | | - // We can't rely on the state transition goroutine because cancel() will kill it |
| 162 | + |
| 163 | + // Shutdown launcher (stops SSE, health monitor, kills core) |
160 | 164 | if launcher != nil { |
161 | 165 | launcher.handleShutdown() |
162 | 166 | } |
163 | | - // IMPORTANT: Quit the tray UI BEFORE cancelling context |
164 | | - // This prevents reconnection attempts from SSE goroutine |
165 | | - logger.Info("Quitting system tray") |
166 | | - trayApp.Quit() |
167 | | - // Now shutdown state machine and cancel context |
| 167 | + |
| 168 | + // Shutdown state machine |
168 | 169 | stateMachine.Shutdown() |
| 170 | + |
| 171 | + // CRITICAL: Cancel context LAST, after all cleanup |
| 172 | + // This prevents the tray.Run() goroutine from quitting prematurely |
| 173 | + logger.Info("Cancelling context after cleanup complete") |
169 | 174 | cancel() |
| 175 | + |
| 176 | + // Give tray.Run() goroutine a moment to see cancellation |
| 177 | + time.Sleep(100 * time.Millisecond) |
| 178 | + |
| 179 | + // Finally, quit the tray UI |
| 180 | + logger.Info("Quitting system tray") |
| 181 | + trayApp.Quit() |
170 | 182 | } |
171 | 183 |
|
172 | 184 | trayApp = tray.NewWithAPIClient(api.NewServerAdapter(apiClient), apiClient, logger.Sugar(), version, shutdownFunc) |
@@ -214,25 +226,25 @@ func main() { |
214 | 226 | go func() { |
215 | 227 | <-sigCh |
216 | 228 | logger.Info("Received shutdown signal") |
217 | | - stateMachine.SendEvent(state.EventShutdown) |
218 | | - // IMPORTANT: Call handleShutdown() directly to terminate core process |
219 | | - // Same as shutdownFunc - we can't rely on state transition goroutine |
220 | | - if launcher != nil { |
221 | | - launcher.handleShutdown() |
222 | | - } |
223 | | - // IMPORTANT: Quit the tray UI BEFORE cancelling context |
224 | | - logger.Info("Quitting system tray from signal handler") |
225 | | - trayApp.Quit() |
226 | | - stateMachine.Shutdown() |
227 | | - cancel() |
| 229 | + |
| 230 | + // Use the same shutdown flow as Quit menu item |
| 231 | + shutdownFunc() |
228 | 232 | }() |
229 | 233 |
|
230 | 234 | logger.Info("Starting tray event loop") |
231 | 235 | if err := trayApp.Run(ctx); err != nil && err != context.Canceled { |
232 | 236 | logger.Error("Tray application error", zap.Error(err)) |
233 | 237 | } |
234 | 238 |
|
235 | | - // Wait for state machine to shut down gracefully |
| 239 | + // Wait for shutdown to complete (with timeout) |
| 240 | + select { |
| 241 | + case <-shutdownComplete: |
| 242 | + logger.Info("Shutdown completed successfully") |
| 243 | + case <-time.After(5 * time.Second): |
| 244 | + logger.Warn("Shutdown timeout - forcing exit") |
| 245 | + } |
| 246 | + |
| 247 | + // Final cleanup |
236 | 248 | stateMachine.Shutdown() |
237 | 249 |
|
238 | 250 | logger.Info("mcpproxy-tray shutdown complete") |
@@ -331,33 +343,6 @@ func resolveCoreURL() string { |
331 | 343 | return defaultCoreURL |
332 | 344 | } |
333 | 345 |
|
334 | | -// isSocketAvailable checks if a socket/pipe endpoint is accessible |
335 | | -func isSocketAvailable(endpoint string) bool { |
336 | | - // Parse endpoint to extract scheme |
337 | | - parsed, err := url.Parse(endpoint) |
338 | | - if err != nil { |
339 | | - return false |
340 | | - } |
341 | | - |
342 | | - switch parsed.Scheme { |
343 | | - case "unix": |
344 | | - // Check if Unix socket file exists |
345 | | - socketPath := parsed.Path |
346 | | - if socketPath == "" { |
347 | | - socketPath = parsed.Opaque |
348 | | - } |
349 | | - _, err := os.Stat(socketPath) |
350 | | - return err == nil |
351 | | - case "npipe": |
352 | | - // For Windows named pipes, we can't easily check existence |
353 | | - // Return true and let the connection attempt fail if needed |
354 | | - return true |
355 | | - default: |
356 | | - // Not a socket endpoint |
357 | | - return false |
358 | | - } |
359 | | -} |
360 | | - |
361 | 346 | func shouldSkipCoreLaunch() bool { |
362 | 347 | value := strings.TrimSpace(os.Getenv("MCPPROXY_TRAY_SKIP_CORE")) |
363 | 348 | return value == "1" || strings.EqualFold(value, "true") |
@@ -1361,15 +1346,25 @@ func (cpl *CoreProcessLauncher) handleGeneralError() { |
1361 | 1346 | func (cpl *CoreProcessLauncher) handleShutdown() { |
1362 | 1347 | cpl.logger.Info("Core process launcher shutting down") |
1363 | 1348 |
|
1364 | | - if cpl.processMonitor != nil { |
1365 | | - cpl.processMonitor.Shutdown() |
1366 | | - } |
| 1349 | + // CRITICAL: Stop SSE FIRST before killing core |
| 1350 | + // This prevents SSE from detecting disconnection and trying to reconnect |
| 1351 | + cpl.logger.Info("Stopping SSE connection") |
| 1352 | + cpl.apiClient.StopSSE() |
1367 | 1353 |
|
| 1354 | + // Give SSE goroutine a moment to see cancellation and exit cleanly |
| 1355 | + time.Sleep(100 * time.Millisecond) |
| 1356 | + |
| 1357 | + // Stop health monitor before killing core |
1368 | 1358 | if cpl.healthMonitor != nil { |
| 1359 | + cpl.logger.Info("Stopping health monitor") |
1369 | 1360 | cpl.healthMonitor.Stop() |
1370 | 1361 | } |
1371 | 1362 |
|
1372 | | - cpl.apiClient.StopSSE() |
| 1363 | + // Finally, kill the core process |
| 1364 | + if cpl.processMonitor != nil { |
| 1365 | + cpl.logger.Info("Shutting down core process") |
| 1366 | + cpl.processMonitor.Shutdown() |
| 1367 | + } |
1373 | 1368 | } |
1374 | 1369 |
|
1375 | 1370 | // buildCoreEnvironment builds the environment for the core process |
|
0 commit comments