chore: checkpoint baseline before wave1 multi-agent
- commit
- edc44c95d4f78470019568071bcb92b4b04944e6
- author
- brice · · 2026-02-07 04:53 UTC
- parent
- c803dee
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +288 -47
M .gitignore +3 -0
| @@ -107,3 +107,6 @@ aur/*/*.pkg.tar.zst | |||
| 107 | 107 | aur/*/LICENSE | |
| 108 | 108 | aur/*/dev.bnema.Dumber.desktop | |
| 109 | 109 | aur/*/logo-512.png | |
| 110 | + | ||
| 111 | + | # Planning/runbook notes stored outside product docs | |
| 112 | + | docs/reference/stability-performance-multi-agent-runbook-*.md | |
| @@ -107,3 +107,6 @@ aur/*/*.pkg.tar.zst | |||||
| 107 | aur/*/LICENSE | 107 | aur/*/LICENSE | ||
| 108 | aur/*/dev.bnema.Dumber.desktop | 108 | aur/*/dev.bnema.Dumber.desktop | ||
| 109 | aur/*/logo-512.png | 109 | aur/*/logo-512.png | ||
| 110 | + | ||||
| 111 | + | # Planning/runbook notes stored outside product docs | |||
| 112 | + | docs/reference/stability-performance-multi-agent-runbook-*.md | |||
M go.mod +2 -2
| @@ -3,7 +3,7 @@ module github.com/bnema/dumber | |||
| 3 | 3 | go 1.25.3 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | github.com/bnema/puregotk-webkit v0.0.9 | |
| 6 | + | github.com/bnema/puregotk-webkit v0.0.10 | |
| 7 | 7 | github.com/charmbracelet/bubbles v0.21.0 | |
| 8 | 8 | github.com/charmbracelet/bubbletea v1.3.10 | |
| 9 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | |
| @@ -22,7 +22,7 @@ require ( | |||
| 22 | 22 | gopkg.in/yaml.v3 v3.0.1 | |
| 23 | 23 | ) | |
| 24 | 24 | ||
| 25 | - | replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20251222174431-73930527faa3 | |
| 25 | + | replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20260207041652-c591276fa9d3 | |
| 26 | 26 | ||
| 27 | 27 | require ( | |
| 28 | 28 | github.com/atotto/clipboard v0.1.4 // indirect | |
| @@ -3,7 +3,7 @@ module github.com/bnema/dumber | |||||
| 3 | go 1.25.3 | 3 | go 1.25.3 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | github.com/bnema/puregotk-webkit v0.0.9 | 6 | + | github.com/bnema/puregotk-webkit v0.0.10 |
| 7 | github.com/charmbracelet/bubbles v0.21.0 | 7 | github.com/charmbracelet/bubbles v0.21.0 | ||
| 8 | github.com/charmbracelet/bubbletea v1.3.10 | 8 | github.com/charmbracelet/bubbletea v1.3.10 | ||
| 9 | github.com/charmbracelet/lipgloss v1.1.0 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | ||
| @@ -22,7 +22,7 @@ require ( | |||||
| 22 | gopkg.in/yaml.v3 v3.0.1 | 22 | gopkg.in/yaml.v3 v3.0.1 | ||
| 23 | ) | 23 | ) | ||
| 24 | 24 | ||||
| 25 | - | replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20251222174431-73930527faa3 | 25 | + | replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-20260207041652-c591276fa9d3 |
| 26 | 26 | ||||
| 27 | require ( | 27 | require ( | ||
| 28 | github.com/atotto/clipboard v0.1.4 // indirect | 28 | github.com/atotto/clipboard v0.1.4 // indirect | ||
M go.sum +4 -4 generated file: collapsed
M internal/bootstrap/session.go +110 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "os" | |
| 8 | 8 | "path/filepath" | |
| 9 | + | "strings" | |
| 9 | 10 | "sync" | |
| 10 | 11 | "syscall" | |
| 11 | 12 | "time" | |
| @@ -24,6 +25,7 @@ const ( | |||
| 24 | 25 | recentSessionsLimit = 200 | |
| 25 | 26 | lockDirPerm = 0o755 | |
| 26 | 27 | lockFilePerm = 0o600 | |
| 28 | + | markerFilePerm = 0o644 | |
| 27 | 29 | ) | |
| 28 | 30 | ||
| 29 | 31 | type BrowserSession struct { | |
| @@ -95,6 +97,19 @@ func StartBrowserSession( | |||
| 95 | 97 | // error handling during shutdown sequences where persistFn may be called multiple times. | |
| 96 | 98 | persistFn := func(persistCtx context.Context) error { | |
| 97 | 99 | persistMu.Do(func() { | |
| 100 | + | if lockDir != "" { | |
| 101 | + | abruptSessions, abruptErr := markAbruptExits(lockDir, now.UTC()) | |
| 102 | + | if abruptErr != nil { | |
| 103 | + | logger.Warn().Err(abruptErr).Msg("failed to check abrupt exit markers") | |
| 104 | + | } else { | |
| 105 | + | for _, abruptSessionID := range abruptSessions { | |
| 106 | + | logger.Warn(). | |
| 107 | + | Str("session_id", abruptSessionID). | |
| 108 | + | Msg("abrupt-exit marker detected (startup without shutdown)") | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | ||
| 98 | 113 | saveCtx := corelogging.WithContext(context.Background(), logger) | |
| 99 | 114 | if err := sessionRepo.Save(saveCtx, session); err != nil { | |
| 100 | 115 | persistErr = fmt.Errorf("save session: %w", err) | |
| @@ -105,6 +120,11 @@ func StartBrowserSession( | |||
| 105 | 120 | Str("session_id", string(session.ID)). | |
| 106 | 121 | Str("type", string(session.Type)). | |
| 107 | 122 | Msg("session started") | |
| 123 | + | if lockDir != "" { | |
| 124 | + | if err := writeStartupMarker(lockDir, string(session.ID), now.UTC()); err != nil { | |
| 125 | + | logger.Warn().Err(err).Str("session_id", string(session.ID)).Msg("failed to write startup marker") | |
| 126 | + | } | |
| 127 | + | } | |
| 108 | 128 | ||
| 109 | 129 | lf, lp, lockErr := lockSessionFile(lockDir, session.ID) | |
| 110 | 130 | if lockErr != nil { | |
| @@ -133,6 +153,13 @@ func StartBrowserSession( | |||
| 133 | 153 | lp := lockPath | |
| 134 | 154 | lockMu.Unlock() | |
| 135 | 155 | ||
| 156 | + | if lockDir != "" { | |
| 157 | + | shutdownAt := time.Now().UTC() | |
| 158 | + | if markerErr := writeShutdownMarker(lockDir, string(session.ID), shutdownAt); markerErr != nil { | |
| 159 | + | logger.Warn().Err(markerErr).Str("session_id", string(session.ID)).Msg("failed to write clean shutdown marker") | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | ||
| 136 | 163 | if lf != nil { | |
| 137 | 164 | _ = unlockAndClose(lf) | |
| 138 | 165 | _ = os.Remove(lp) | |
| @@ -263,6 +290,89 @@ func unlockAndClose(f *os.File) error { | |||
| 263 | 290 | return f.Close() | |
| 264 | 291 | } | |
| 265 | 292 | ||
| 293 | + | func startupMarkerPath(lockDir, sessionID string) string { | |
| 294 | + | return filepath.Join(lockDir, fmt.Sprintf("session_%s.startup.marker", sessionID)) | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | func shutdownMarkerPath(lockDir, sessionID string) string { | |
| 298 | + | return filepath.Join(lockDir, fmt.Sprintf("session_%s.shutdown.marker", sessionID)) | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | func abruptMarkerPath(lockDir, sessionID string) string { | |
| 302 | + | return filepath.Join(lockDir, fmt.Sprintf("session_%s.abrupt.marker", sessionID)) | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | func writeStartupMarker(lockDir, sessionID string, startedAt time.Time) error { | |
| 306 | + | if lockDir == "" || sessionID == "" { | |
| 307 | + | return errors.New("startup marker requires lockDir and sessionID") | |
| 308 | + | } | |
| 309 | + | if err := os.MkdirAll(lockDir, lockDirPerm); err != nil { | |
| 310 | + | return err | |
| 311 | + | } | |
| 312 | + | content := []byte(startedAt.Format(time.RFC3339Nano) + "\n") | |
| 313 | + | if err := os.WriteFile(startupMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | |
| 314 | + | return err | |
| 315 | + | } | |
| 316 | + | _ = os.Remove(shutdownMarkerPath(lockDir, sessionID)) | |
| 317 | + | _ = os.Remove(abruptMarkerPath(lockDir, sessionID)) | |
| 318 | + | return nil | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | func writeShutdownMarker(lockDir, sessionID string, endedAt time.Time) error { | |
| 322 | + | if lockDir == "" || sessionID == "" { | |
| 323 | + | return errors.New("shutdown marker requires lockDir and sessionID") | |
| 324 | + | } | |
| 325 | + | if err := os.MkdirAll(lockDir, lockDirPerm); err != nil { | |
| 326 | + | return err | |
| 327 | + | } | |
| 328 | + | content := []byte(endedAt.Format(time.RFC3339Nano) + "\n") | |
| 329 | + | if err := os.WriteFile(shutdownMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | |
| 330 | + | return err | |
| 331 | + | } | |
| 332 | + | return nil | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | func markAbruptExits(lockDir string, detectedAt time.Time) ([]string, error) { | |
| 336 | + | if lockDir == "" { | |
| 337 | + | return nil, nil | |
| 338 | + | } | |
| 339 | + | pattern := filepath.Join(lockDir, "session_*.startup.marker") | |
| 340 | + | startupMarkers, err := filepath.Glob(pattern) | |
| 341 | + | if err != nil { | |
| 342 | + | return nil, err | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | abruptSessions := make([]string, 0, len(startupMarkers)) | |
| 346 | + | for _, startupPath := range startupMarkers { | |
| 347 | + | base := filepath.Base(startupPath) | |
| 348 | + | if !strings.HasPrefix(base, "session_") || !strings.HasSuffix(base, ".startup.marker") { | |
| 349 | + | continue | |
| 350 | + | } | |
| 351 | + | sessionID := strings.TrimSuffix(strings.TrimPrefix(base, "session_"), ".startup.marker") | |
| 352 | + | if sessionID == "" { | |
| 353 | + | continue | |
| 354 | + | } | |
| 355 | + | if _, err := os.Stat(shutdownMarkerPath(lockDir, sessionID)); err == nil { | |
| 356 | + | continue | |
| 357 | + | } else if !os.IsNotExist(err) { | |
| 358 | + | return abruptSessions, err | |
| 359 | + | } | |
| 360 | + | if _, err := os.Stat(abruptMarkerPath(lockDir, sessionID)); err == nil { | |
| 361 | + | continue | |
| 362 | + | } else if !os.IsNotExist(err) { | |
| 363 | + | return abruptSessions, err | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | payload := []byte(fmt.Sprintf("detected_at=%s\nstartup_marker=%s\n", detectedAt.Format(time.RFC3339Nano), startupPath)) | |
| 367 | + | if err := os.WriteFile(abruptMarkerPath(lockDir, sessionID), payload, markerFilePerm); err != nil { | |
| 368 | + | return abruptSessions, err | |
| 369 | + | } | |
| 370 | + | abruptSessions = append(abruptSessions, sessionID) | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | return abruptSessions, nil | |
| 374 | + | } | |
| 375 | + | ||
| 266 | 376 | // runSessionCleanupAsync performs stale session cleanup and old session pruning | |
| 267 | 377 | // in a background goroutine. This avoids blocking startup for non-critical tasks. | |
| 268 | 378 | // Uses context.Background() to ensure cleanup completes even if startup context is canceled. | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "path/filepath" | 8 | "path/filepath" | ||
| 9 | + | "strings" | |||
| 9 | "sync" | 10 | "sync" | ||
| 10 | "syscall" | 11 | "syscall" | ||
| 11 | "time" | 12 | "time" | ||
| @@ -24,6 +25,7 @@ const ( | |||||
| 24 | recentSessionsLimit = 200 | 25 | recentSessionsLimit = 200 | ||
| 25 | lockDirPerm = 0o755 | 26 | lockDirPerm = 0o755 | ||
| 26 | lockFilePerm = 0o600 | 27 | lockFilePerm = 0o600 | ||
| 28 | + | markerFilePerm = 0o644 | |||
| 27 | ) | 29 | ) | ||
| 28 | 30 | ||||
| 29 | type BrowserSession struct { | 31 | type BrowserSession struct { | ||
| @@ -95,6 +97,19 @@ func StartBrowserSession( | |||||
| 95 | // error handling during shutdown sequences where persistFn may be called multiple times. | 97 | // error handling during shutdown sequences where persistFn may be called multiple times. | ||
| 96 | persistFn := func(persistCtx context.Context) error { | 98 | persistFn := func(persistCtx context.Context) error { | ||
| 97 | persistMu.Do(func() { | 99 | persistMu.Do(func() { | ||
| 100 | + | if lockDir != "" { | |||
| 101 | + | abruptSessions, abruptErr := markAbruptExits(lockDir, now.UTC()) | |||
| 102 | + | if abruptErr != nil { | |||
| 103 | + | logger.Warn().Err(abruptErr).Msg("failed to check abrupt exit markers") | |||
| 104 | + | } else { | |||
| 105 | + | for _, abruptSessionID := range abruptSessions { | |||
| 106 | + | logger.Warn(). | |||
| 107 | + | Str("session_id", abruptSessionID). | |||
| 108 | + | Msg("abrupt-exit marker detected (startup without shutdown)") | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | } | |||
| 112 | + | ||||
| 98 | saveCtx := corelogging.WithContext(context.Background(), logger) | 113 | saveCtx := corelogging.WithContext(context.Background(), logger) | ||
| 99 | if err := sessionRepo.Save(saveCtx, session); err != nil { | 114 | if err := sessionRepo.Save(saveCtx, session); err != nil { | ||
| 100 | persistErr = fmt.Errorf("save session: %w", err) | 115 | persistErr = fmt.Errorf("save session: %w", err) | ||
| @@ -105,6 +120,11 @@ func StartBrowserSession( | |||||
| 105 | Str("session_id", string(session.ID)). | 120 | Str("session_id", string(session.ID)). | ||
| 106 | Str("type", string(session.Type)). | 121 | Str("type", string(session.Type)). | ||
| 107 | Msg("session started") | 122 | Msg("session started") | ||
| 123 | + | if lockDir != "" { | |||
| 124 | + | if err := writeStartupMarker(lockDir, string(session.ID), now.UTC()); err != nil { | |||
| 125 | + | logger.Warn().Err(err).Str("session_id", string(session.ID)).Msg("failed to write startup marker") | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 108 | 128 | ||||
| 109 | lf, lp, lockErr := lockSessionFile(lockDir, session.ID) | 129 | lf, lp, lockErr := lockSessionFile(lockDir, session.ID) | ||
| 110 | if lockErr != nil { | 130 | if lockErr != nil { | ||
| @@ -133,6 +153,13 @@ func StartBrowserSession( | |||||
| 133 | lp := lockPath | 153 | lp := lockPath | ||
| 134 | lockMu.Unlock() | 154 | lockMu.Unlock() | ||
| 135 | 155 | ||||
| 156 | + | if lockDir != "" { | |||
| 157 | + | shutdownAt := time.Now().UTC() | |||
| 158 | + | if markerErr := writeShutdownMarker(lockDir, string(session.ID), shutdownAt); markerErr != nil { | |||
| 159 | + | logger.Warn().Err(markerErr).Str("session_id", string(session.ID)).Msg("failed to write clean shutdown marker") | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | ||||
| 136 | if lf != nil { | 163 | if lf != nil { | ||
| 137 | _ = unlockAndClose(lf) | 164 | _ = unlockAndClose(lf) | ||
| 138 | _ = os.Remove(lp) | 165 | _ = os.Remove(lp) | ||
| @@ -263,6 +290,89 @@ func unlockAndClose(f *os.File) error { | |||||
| 263 | return f.Close() | 290 | return f.Close() | ||
| 264 | } | 291 | } | ||
| 265 | 292 | ||||
| 293 | + | func startupMarkerPath(lockDir, sessionID string) string { | |||
| 294 | + | return filepath.Join(lockDir, fmt.Sprintf("session_%s.startup.marker", sessionID)) | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | func shutdownMarkerPath(lockDir, sessionID string) string { | |||
| 298 | + | return filepath.Join(lockDir, fmt.Sprintf("session_%s.shutdown.marker", sessionID)) | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | func abruptMarkerPath(lockDir, sessionID string) string { | |||
| 302 | + | return filepath.Join(lockDir, fmt.Sprintf("session_%s.abrupt.marker", sessionID)) | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | func writeStartupMarker(lockDir, sessionID string, startedAt time.Time) error { | |||
| 306 | + | if lockDir == "" || sessionID == "" { | |||
| 307 | + | return errors.New("startup marker requires lockDir and sessionID") | |||
| 308 | + | } | |||
| 309 | + | if err := os.MkdirAll(lockDir, lockDirPerm); err != nil { | |||
| 310 | + | return err | |||
| 311 | + | } | |||
| 312 | + | content := []byte(startedAt.Format(time.RFC3339Nano) + "\n") | |||
| 313 | + | if err := os.WriteFile(startupMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | |||
| 314 | + | return err | |||
| 315 | + | } | |||
| 316 | + | _ = os.Remove(shutdownMarkerPath(lockDir, sessionID)) | |||
| 317 | + | _ = os.Remove(abruptMarkerPath(lockDir, sessionID)) | |||
| 318 | + | return nil | |||
| 319 | + | } | |||
| 320 | + | ||||
| 321 | + | func writeShutdownMarker(lockDir, sessionID string, endedAt time.Time) error { | |||
| 322 | + | if lockDir == "" || sessionID == "" { | |||
| 323 | + | return errors.New("shutdown marker requires lockDir and sessionID") | |||
| 324 | + | } | |||
| 325 | + | if err := os.MkdirAll(lockDir, lockDirPerm); err != nil { | |||
| 326 | + | return err | |||
| 327 | + | } | |||
| 328 | + | content := []byte(endedAt.Format(time.RFC3339Nano) + "\n") | |||
| 329 | + | if err := os.WriteFile(shutdownMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | |||
| 330 | + | return err | |||
| 331 | + | } | |||
| 332 | + | return nil | |||
| 333 | + | } | |||
| 334 | + | ||||
| 335 | + | func markAbruptExits(lockDir string, detectedAt time.Time) ([]string, error) { | |||
| 336 | + | if lockDir == "" { | |||
| 337 | + | return nil, nil | |||
| 338 | + | } | |||
| 339 | + | pattern := filepath.Join(lockDir, "session_*.startup.marker") | |||
| 340 | + | startupMarkers, err := filepath.Glob(pattern) | |||
| 341 | + | if err != nil { | |||
| 342 | + | return nil, err | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | abruptSessions := make([]string, 0, len(startupMarkers)) | |||
| 346 | + | for _, startupPath := range startupMarkers { | |||
| 347 | + | base := filepath.Base(startupPath) | |||
| 348 | + | if !strings.HasPrefix(base, "session_") || !strings.HasSuffix(base, ".startup.marker") { | |||
| 349 | + | continue | |||
| 350 | + | } | |||
| 351 | + | sessionID := strings.TrimSuffix(strings.TrimPrefix(base, "session_"), ".startup.marker") | |||
| 352 | + | if sessionID == "" { | |||
| 353 | + | continue | |||
| 354 | + | } | |||
| 355 | + | if _, err := os.Stat(shutdownMarkerPath(lockDir, sessionID)); err == nil { | |||
| 356 | + | continue | |||
| 357 | + | } else if !os.IsNotExist(err) { | |||
| 358 | + | return abruptSessions, err | |||
| 359 | + | } | |||
| 360 | + | if _, err := os.Stat(abruptMarkerPath(lockDir, sessionID)); err == nil { | |||
| 361 | + | continue | |||
| 362 | + | } else if !os.IsNotExist(err) { | |||
| 363 | + | return abruptSessions, err | |||
| 364 | + | } | |||
| 365 | + | ||||
| 366 | + | payload := []byte(fmt.Sprintf("detected_at=%s\nstartup_marker=%s\n", detectedAt.Format(time.RFC3339Nano), startupPath)) | |||
| 367 | + | if err := os.WriteFile(abruptMarkerPath(lockDir, sessionID), payload, markerFilePerm); err != nil { | |||
| 368 | + | return abruptSessions, err | |||
| 369 | + | } | |||
| 370 | + | abruptSessions = append(abruptSessions, sessionID) | |||
| 371 | + | } | |||
| 372 | + | ||||
| 373 | + | return abruptSessions, nil | |||
| 374 | + | } | |||
| 375 | + | ||||
| 266 | // runSessionCleanupAsync performs stale session cleanup and old session pruning | 376 | // runSessionCleanupAsync performs stale session cleanup and old session pruning | ||
| 267 | // in a background goroutine. This avoids blocking startup for non-critical tasks. | 377 | // in a background goroutine. This avoids blocking startup for non-critical tasks. | ||
| 268 | // Uses context.Background() to ensure cleanup completes even if startup context is canceled. | 378 | // Uses context.Background() to ensure cleanup completes even if startup context is canceled. | ||
A internal/bootstrap/session_markers_test.go +65 -0
| @@ -0,0 +1,65 @@ | |||
| 1 | + | package bootstrap | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "path/filepath" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/assert" | |
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestSessionMarkers_WriteStartupAndShutdown(t *testing.T) { | |
| 14 | + | lockDir := t.TempDir() | |
| 15 | + | sessionID := "s123" | |
| 16 | + | startedAt := time.Date(2026, 2, 7, 10, 0, 0, 0, time.UTC) | |
| 17 | + | endedAt := startedAt.Add(3 * time.Minute) | |
| 18 | + | ||
| 19 | + | err := writeStartupMarker(lockDir, sessionID, startedAt) | |
| 20 | + | require.NoError(t, err) | |
| 21 | + | ||
| 22 | + | startupContent, err := os.ReadFile(startupMarkerPath(lockDir, sessionID)) | |
| 23 | + | require.NoError(t, err) | |
| 24 | + | assert.Contains(t, string(startupContent), startedAt.Format(time.RFC3339Nano)) | |
| 25 | + | ||
| 26 | + | err = writeShutdownMarker(lockDir, sessionID, endedAt) | |
| 27 | + | require.NoError(t, err) | |
| 28 | + | ||
| 29 | + | shutdownContent, err := os.ReadFile(shutdownMarkerPath(lockDir, sessionID)) | |
| 30 | + | require.NoError(t, err) | |
| 31 | + | assert.Contains(t, string(shutdownContent), endedAt.Format(time.RFC3339Nano)) | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func TestSessionMarkers_MarkAbruptExits(t *testing.T) { | |
| 35 | + | lockDir := t.TempDir() | |
| 36 | + | detectedAt := time.Date(2026, 2, 7, 11, 0, 0, 0, time.UTC) | |
| 37 | + | ||
| 38 | + | require.NoError(t, writeStartupMarker(lockDir, "abrupt-a", detectedAt.Add(-5*time.Minute))) | |
| 39 | + | require.NoError(t, writeStartupMarker(lockDir, "clean-b", detectedAt.Add(-4*time.Minute))) | |
| 40 | + | require.NoError(t, writeShutdownMarker(lockDir, "clean-b", detectedAt.Add(-1*time.Minute))) | |
| 41 | + | ||
| 42 | + | abruptSessions, err := markAbruptExits(lockDir, detectedAt) | |
| 43 | + | require.NoError(t, err) | |
| 44 | + | assert.Equal(t, []string{"abrupt-a"}, abruptSessions) | |
| 45 | + | ||
| 46 | + | abruptMarkerData, err := os.ReadFile(abruptMarkerPath(lockDir, "abrupt-a")) | |
| 47 | + | require.NoError(t, err) | |
| 48 | + | assert.Contains(t, string(abruptMarkerData), "detected_at="+detectedAt.Format(time.RFC3339Nano)) | |
| 49 | + | assert.FileExists(t, filepath.Join(lockDir, "session_clean-b.shutdown.marker")) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func TestSessionMarkers_MarkAbruptExitsIdempotent(t *testing.T) { | |
| 53 | + | lockDir := t.TempDir() | |
| 54 | + | detectedAt := time.Date(2026, 2, 7, 12, 0, 0, 0, time.UTC) | |
| 55 | + | ||
| 56 | + | require.NoError(t, writeStartupMarker(lockDir, "abrupt-c", detectedAt.Add(-10*time.Minute))) | |
| 57 | + | ||
| 58 | + | first, err := markAbruptExits(lockDir, detectedAt) | |
| 59 | + | require.NoError(t, err) | |
| 60 | + | assert.Equal(t, []string{"abrupt-c"}, first) | |
| 61 | + | ||
| 62 | + | second, err := markAbruptExits(lockDir, detectedAt.Add(1*time.Minute)) | |
| 63 | + | require.NoError(t, err) | |
| 64 | + | assert.Empty(t, second) | |
| 65 | + | } | |
| @@ -0,0 +1,65 @@ | |||||
| 1 | + | package bootstrap | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/stretchr/testify/assert" | |||
| 10 | + | "github.com/stretchr/testify/require" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestSessionMarkers_WriteStartupAndShutdown(t *testing.T) { | |||
| 14 | + | lockDir := t.TempDir() | |||
| 15 | + | sessionID := "s123" | |||
| 16 | + | startedAt := time.Date(2026, 2, 7, 10, 0, 0, 0, time.UTC) | |||
| 17 | + | endedAt := startedAt.Add(3 * time.Minute) | |||
| 18 | + | ||||
| 19 | + | err := writeStartupMarker(lockDir, sessionID, startedAt) | |||
| 20 | + | require.NoError(t, err) | |||
| 21 | + | ||||
| 22 | + | startupContent, err := os.ReadFile(startupMarkerPath(lockDir, sessionID)) | |||
| 23 | + | require.NoError(t, err) | |||
| 24 | + | assert.Contains(t, string(startupContent), startedAt.Format(time.RFC3339Nano)) | |||
| 25 | + | ||||
| 26 | + | err = writeShutdownMarker(lockDir, sessionID, endedAt) | |||
| 27 | + | require.NoError(t, err) | |||
| 28 | + | ||||
| 29 | + | shutdownContent, err := os.ReadFile(shutdownMarkerPath(lockDir, sessionID)) | |||
| 30 | + | require.NoError(t, err) | |||
| 31 | + | assert.Contains(t, string(shutdownContent), endedAt.Format(time.RFC3339Nano)) | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func TestSessionMarkers_MarkAbruptExits(t *testing.T) { | |||
| 35 | + | lockDir := t.TempDir() | |||
| 36 | + | detectedAt := time.Date(2026, 2, 7, 11, 0, 0, 0, time.UTC) | |||
| 37 | + | ||||
| 38 | + | require.NoError(t, writeStartupMarker(lockDir, "abrupt-a", detectedAt.Add(-5*time.Minute))) | |||
| 39 | + | require.NoError(t, writeStartupMarker(lockDir, "clean-b", detectedAt.Add(-4*time.Minute))) | |||
| 40 | + | require.NoError(t, writeShutdownMarker(lockDir, "clean-b", detectedAt.Add(-1*time.Minute))) | |||
| 41 | + | ||||
| 42 | + | abruptSessions, err := markAbruptExits(lockDir, detectedAt) | |||
| 43 | + | require.NoError(t, err) | |||
| 44 | + | assert.Equal(t, []string{"abrupt-a"}, abruptSessions) | |||
| 45 | + | ||||
| 46 | + | abruptMarkerData, err := os.ReadFile(abruptMarkerPath(lockDir, "abrupt-a")) | |||
| 47 | + | require.NoError(t, err) | |||
| 48 | + | assert.Contains(t, string(abruptMarkerData), "detected_at="+detectedAt.Format(time.RFC3339Nano)) | |||
| 49 | + | assert.FileExists(t, filepath.Join(lockDir, "session_clean-b.shutdown.marker")) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func TestSessionMarkers_MarkAbruptExitsIdempotent(t *testing.T) { | |||
| 53 | + | lockDir := t.TempDir() | |||
| 54 | + | detectedAt := time.Date(2026, 2, 7, 12, 0, 0, 0, time.UTC) | |||
| 55 | + | ||||
| 56 | + | require.NoError(t, writeStartupMarker(lockDir, "abrupt-c", detectedAt.Add(-10*time.Minute))) | |||
| 57 | + | ||||
| 58 | + | first, err := markAbruptExits(lockDir, detectedAt) | |||
| 59 | + | require.NoError(t, err) | |||
| 60 | + | assert.Equal(t, []string{"abrupt-c"}, first) | |||
| 61 | + | ||||
| 62 | + | second, err := markAbruptExits(lockDir, detectedAt.Add(1*time.Minute)) | |||
| 63 | + | require.NoError(t, err) | |||
| 64 | + | assert.Empty(t, second) | |||
| 65 | + | } | |||
A internal/infrastructure/config/defaults_test.go +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | package config | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/assert" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestDefaultConfig_RuntimeLoggingProfile(t *testing.T) { | |
| 10 | + | cfg := DefaultConfig() | |
| 11 | + | assert.Equal(t, "info", cfg.Logging.Level) | |
| 12 | + | assert.False(t, cfg.Logging.CaptureGTKLogs) | |
| 13 | + | } | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | package config | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/assert" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestDefaultConfig_RuntimeLoggingProfile(t *testing.T) { | |||
| 10 | + | cfg := DefaultConfig() | |||
| 11 | + | assert.Equal(t, "info", cfg.Logging.Level) | |||
| 12 | + | assert.False(t, cfg.Logging.CaptureGTKLogs) | |||
| 13 | + | } | |||
M internal/infrastructure/webkit/messaging.go +4 -4
| @@ -146,10 +146,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||
| 146 | 146 | ||
| 147 | 147 | // Connect to "script-message-received::dumber" with handler name as signal detail | |
| 148 | 148 | signalID := ucm.ConnectScriptMessageReceivedWithDetail(MessageHandlerName, &cb) | |
| 149 | - | log.Debug().Uint32("signal_id", signalID).Str("handler", MessageHandlerName).Msg("signal connected with detail") | |
| 149 | + | log.Debug().Uint32("signal_id", uint32(signalID)).Str("handler", MessageHandlerName).Msg("signal connected with detail") | |
| 150 | 150 | ||
| 151 | 151 | r.mu.Lock() | |
| 152 | - | r.signals = append(r.signals, signalID) | |
| 152 | + | r.signals = append(r.signals, uint32(signalID)) | |
| 153 | 153 | r.mu.Unlock() | |
| 154 | 154 | ||
| 155 | 155 | log.Debug().Msg("SetupMessageHandler: registering handler in main world") | |
| @@ -164,10 +164,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||
| 164 | 164 | log.Info(). | |
| 165 | 165 | Str("handler", MessageHandlerName). | |
| 166 | 166 | Str("world", "main"). | |
| 167 | - | Uint32("signal_id", signalID). | |
| 167 | + | Uint32("signal_id", uint32(signalID)). | |
| 168 | 168 | Msg("script message handler connected") | |
| 169 | 169 | ||
| 170 | - | return signalID, nil | |
| 170 | + | return uint32(signalID), nil | |
| 171 | 171 | } | |
| 172 | 172 | ||
| 173 | 173 | // handleScriptMessage decodes the JSC value and routes it to the correct handler. | |
| @@ -146,10 +146,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||||
| 146 | 146 | ||||
| 147 | // Connect to "script-message-received::dumber" with handler name as signal detail | 147 | // Connect to "script-message-received::dumber" with handler name as signal detail | ||
| 148 | signalID := ucm.ConnectScriptMessageReceivedWithDetail(MessageHandlerName, &cb) | 148 | signalID := ucm.ConnectScriptMessageReceivedWithDetail(MessageHandlerName, &cb) | ||
| 149 | - | log.Debug().Uint32("signal_id", signalID).Str("handler", MessageHandlerName).Msg("signal connected with detail") | 149 | + | log.Debug().Uint32("signal_id", uint32(signalID)).Str("handler", MessageHandlerName).Msg("signal connected with detail") |
| 150 | 150 | ||||
| 151 | r.mu.Lock() | 151 | r.mu.Lock() | ||
| 152 | - | r.signals = append(r.signals, signalID) | 152 | + | r.signals = append(r.signals, uint32(signalID)) |
| 153 | r.mu.Unlock() | 153 | r.mu.Unlock() | ||
| 154 | 154 | ||||
| 155 | log.Debug().Msg("SetupMessageHandler: registering handler in main world") | 155 | log.Debug().Msg("SetupMessageHandler: registering handler in main world") | ||
| @@ -164,10 +164,10 @@ func (r *MessageRouter) SetupMessageHandler(ucm *webkit.UserContentManager, _ st | |||||
| 164 | log.Info(). | 164 | log.Info(). | ||
| 165 | Str("handler", MessageHandlerName). | 165 | Str("handler", MessageHandlerName). | ||
| 166 | Str("world", "main"). | 166 | Str("world", "main"). | ||
| 167 | - | Uint32("signal_id", signalID). | 167 | + | Uint32("signal_id", uint32(signalID)). |
| 168 | Msg("script message handler connected") | 168 | Msg("script message handler connected") | ||
| 169 | 169 | ||||
| 170 | - | return signalID, nil | 170 | + | return uint32(signalID), nil |
| 171 | } | 171 | } | ||
| 172 | 172 | ||||
| 173 | // handleScriptMessage decodes the JSC value and routes it to the correct handler. | 173 | // handleScriptMessage decodes the JSC value and routes it to the correct handler. | ||
M internal/infrastructure/webkit/webview.go +68 -33
| @@ -138,19 +138,20 @@ type WebView struct { | |||
| 138 | 138 | signalIDs []uint32 | |
| 139 | 139 | ||
| 140 | 140 | // Callbacks (set by UI layer) | |
| 141 | - | OnLoadChanged func(LoadEvent) | |
| 142 | - | OnTitleChanged func(string) | |
| 143 | - | OnURIChanged func(string) | |
| 144 | - | OnProgressChanged func(float64) | |
| 145 | - | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes | |
| 146 | - | OnClose func() | |
| 147 | - | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | |
| 148 | - | OnReadyToShow func() // Called when popup is ready to display | |
| 149 | - | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) | |
| 150 | - | OnEnterFullscreen func() bool // Return true to prevent fullscreen | |
| 151 | - | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen | |
| 152 | - | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | |
| 153 | - | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) | |
| 141 | + | OnLoadChanged func(LoadEvent) | |
| 142 | + | OnTitleChanged func(string) | |
| 143 | + | OnURIChanged func(string) | |
| 144 | + | OnProgressChanged func(float64) | |
| 145 | + | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes | |
| 146 | + | OnClose func() | |
| 147 | + | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | |
| 148 | + | OnReadyToShow func() // Called when popup is ready to display | |
| 149 | + | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) | |
| 150 | + | OnEnterFullscreen func() bool // Return true to prevent fullscreen | |
| 151 | + | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen | |
| 152 | + | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | |
| 153 | + | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) | |
| 154 | + | OnWebProcessTerminated func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) | |
| 154 | 155 | ||
| 155 | 156 | logger zerolog.Logger | |
| 156 | 157 | mu sync.RWMutex | |
| @@ -241,7 +242,7 @@ func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint | |||
| 241 | 242 | cb := func(_ webkit.FindController, matchCount uint) { | |
| 242 | 243 | callback(matchCount) | |
| 243 | 244 | } | |
| 244 | - | return a.fc.ConnectFoundText(&cb) | |
| 245 | + | return uint32(a.fc.ConnectFoundText(&cb)) | |
| 245 | 246 | } | |
| 246 | 247 | ||
| 247 | 248 | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | |
| @@ -251,7 +252,7 @@ func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | |||
| 251 | 252 | cb := func(_ webkit.FindController) { | |
| 252 | 253 | callback() | |
| 253 | 254 | } | |
| 254 | - | return a.fc.ConnectFailedToFindText(&cb) | |
| 255 | + | return uint32(a.fc.ConnectFailedToFindText(&cb)) | |
| 255 | 256 | } | |
| 256 | 257 | ||
| 257 | 258 | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint32 { | |
| @@ -261,7 +262,7 @@ func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) | |||
| 261 | 262 | cb := func(_ webkit.FindController, matchCount uint) { | |
| 262 | 263 | callback(matchCount) | |
| 263 | 264 | } | |
| 264 | - | return a.fc.ConnectCountedMatches(&cb) | |
| 265 | + | return uint32(a.fc.ConnectCountedMatches(&cb)) | |
| 265 | 266 | } | |
| 266 | 267 | ||
| 267 | 268 | func (a *findControllerAdapter) DisconnectSignal(id uint32) { | |
| @@ -269,7 +270,7 @@ func (a *findControllerAdapter) DisconnectSignal(id uint32) { | |||
| 269 | 270 | return | |
| 270 | 271 | } | |
| 271 | 272 | obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer()) | |
| 272 | - | gobject.SignalHandlerDisconnect(obj, id) | |
| 273 | + | gobject.SignalHandlerDisconnect(obj, uint(id)) | |
| 273 | 274 | } | |
| 274 | 275 | ||
| 275 | 276 | // NewWebView creates a new WebView with the given context and settings. | |
| @@ -388,6 +389,7 @@ func (wv *WebView) connectSignals() { | |||
| 388 | 389 | wv.connectAudioStateSignal() | |
| 389 | 390 | wv.connectMouseTargetChangedSignal() | |
| 390 | 391 | wv.connectBackForwardListChangedSignal() | |
| 392 | + | wv.connectWebProcessTerminatedSignal() | |
| 391 | 393 | } | |
| 392 | 394 | ||
| 393 | 395 | func (wv *WebView) connectLoadChangedSignal() { | |
| @@ -421,7 +423,7 @@ func (wv *WebView) connectLoadChangedSignal() { | |||
| 421 | 423 | } | |
| 422 | 424 | } | |
| 423 | 425 | sigID := wv.inner.ConnectLoadChanged(&loadChangedCb) | |
| 424 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 426 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 425 | 427 | } | |
| 426 | 428 | ||
| 427 | 429 | func (wv *WebView) connectCloseSignal() { | |
| @@ -431,7 +433,7 @@ func (wv *WebView) connectCloseSignal() { | |||
| 431 | 433 | } | |
| 432 | 434 | } | |
| 433 | 435 | sigID := wv.inner.ConnectClose(&closeCb) | |
| 434 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 436 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 435 | 437 | } | |
| 436 | 438 | ||
| 437 | 439 | func (wv *WebView) connectCreateSignal() { | |
| @@ -473,7 +475,7 @@ func (wv *WebView) connectCreateSignal() { | |||
| 473 | 475 | return newWV.inner.Widget | |
| 474 | 476 | } | |
| 475 | 477 | sigID := wv.inner.ConnectCreate(&createCb) | |
| 476 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 478 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 477 | 479 | } | |
| 478 | 480 | ||
| 479 | 481 | func (wv *WebView) connectReadyToShowSignal() { | |
| @@ -483,7 +485,7 @@ func (wv *WebView) connectReadyToShowSignal() { | |||
| 483 | 485 | } | |
| 484 | 486 | } | |
| 485 | 487 | sigID := wv.inner.ConnectReadyToShow(&readyToShowCb) | |
| 486 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 488 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 487 | 489 | } | |
| 488 | 490 | ||
| 489 | 491 | func (wv *WebView) connectTitleSignal() { | |
| @@ -498,7 +500,7 @@ func (wv *WebView) connectTitleSignal() { | |||
| 498 | 500 | } | |
| 499 | 501 | } | |
| 500 | 502 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::title", glib.NewCallback(&titleCb)) | |
| 501 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 503 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 502 | 504 | } | |
| 503 | 505 | ||
| 504 | 506 | func (wv *WebView) connectURISignal() { | |
| @@ -514,7 +516,7 @@ func (wv *WebView) connectURISignal() { | |||
| 514 | 516 | } | |
| 515 | 517 | } | |
| 516 | 518 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::uri", glib.NewCallback(&uriCb)) | |
| 517 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 519 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 518 | 520 | } | |
| 519 | 521 | ||
| 520 | 522 | func (wv *WebView) connectFaviconSignal() { | |
| @@ -525,7 +527,7 @@ func (wv *WebView) connectFaviconSignal() { | |||
| 525 | 527 | } | |
| 526 | 528 | } | |
| 527 | 529 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::favicon", glib.NewCallback(&faviconCb)) | |
| 528 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 530 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 529 | 531 | } | |
| 530 | 532 | ||
| 531 | 533 | // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead. | |
| @@ -555,7 +557,7 @@ func (wv *WebView) connectProgressSignal() { | |||
| 555 | 557 | } | |
| 556 | 558 | } | |
| 557 | 559 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::estimated-load-progress", glib.NewCallback(&progressCb)) | |
| 558 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 560 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 559 | 561 | } | |
| 560 | 562 | ||
| 561 | 563 | func (wv *WebView) connectDecidePolicySignal() { | |
| @@ -571,7 +573,7 @@ func (wv *WebView) connectDecidePolicySignal() { | |||
| 571 | 573 | } | |
| 572 | 574 | } | |
| 573 | 575 | sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb) | |
| 574 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 576 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 575 | 577 | } | |
| 576 | 578 | ||
| 577 | 579 | // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads). | |
| @@ -770,7 +772,7 @@ func (wv *WebView) connectEnterFullscreenSignal() { | |||
| 770 | 772 | return false // Allow fullscreen | |
| 771 | 773 | } | |
| 772 | 774 | sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb) | |
| 773 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 775 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 774 | 776 | } | |
| 775 | 777 | ||
| 776 | 778 | func (wv *WebView) connectLeaveFullscreenSignal() { | |
| @@ -783,7 +785,7 @@ func (wv *WebView) connectLeaveFullscreenSignal() { | |||
| 783 | 785 | return false // Allow leaving fullscreen | |
| 784 | 786 | } | |
| 785 | 787 | sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb) | |
| 786 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 788 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 787 | 789 | } | |
| 788 | 790 | ||
| 789 | 791 | func (wv *WebView) connectAudioStateSignal() { | |
| @@ -801,7 +803,7 @@ func (wv *WebView) connectAudioStateSignal() { | |||
| 801 | 803 | } | |
| 802 | 804 | } | |
| 803 | 805 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | |
| 804 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 806 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 805 | 807 | } | |
| 806 | 808 | ||
| 807 | 809 | func (wv *WebView) connectMouseTargetChangedSignal() { | |
| @@ -829,7 +831,7 @@ func (wv *WebView) connectMouseTargetChangedSignal() { | |||
| 829 | 831 | wv.OnLinkHover(uri) | |
| 830 | 832 | } | |
| 831 | 833 | sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb) | |
| 832 | - | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 834 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 833 | 835 | } | |
| 834 | 836 | ||
| 835 | 837 | func (wv *WebView) connectBackForwardListChangedSignal() { | |
| @@ -857,7 +859,39 @@ func (wv *WebView) connectBackForwardListChangedSignal() { | |||
| 857 | 859 | } | |
| 858 | 860 | sigID := backForwardList.ConnectChanged(&changedCb) | |
| 859 | 861 | wv.backForwardList = backForwardList | |
| 860 | - | wv.backForwardListSignalID = sigID | |
| 862 | + | wv.backForwardListSignalID = uint32(sigID) | |
| 863 | + | } | |
| 864 | + | ||
| 865 | + | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | |
| 866 | + | switch reason { | |
| 867 | + | case webkit.WebProcessCrashedValue: | |
| 868 | + | return "crashed" | |
| 869 | + | case webkit.WebProcessExceededMemoryLimitValue: | |
| 870 | + | return "exceeded_memory" | |
| 871 | + | case webkit.WebProcessTerminatedByApiValue: | |
| 872 | + | return "terminated_by_api" | |
| 873 | + | default: | |
| 874 | + | return "unknown" | |
| 875 | + | } | |
| 876 | + | } | |
| 877 | + | ||
| 878 | + | func (wv *WebView) connectWebProcessTerminatedSignal() { | |
| 879 | + | terminatedCb := func(_ webkit.WebView, reason webkit.WebProcessTerminationReason) { | |
| 880 | + | uri := wv.URI() | |
| 881 | + | reasonLabel := webProcessTerminationReasonString(reason) | |
| 882 | + | wv.logger.Warn(). | |
| 883 | + | Uint64("id", uint64(wv.id)). | |
| 884 | + | Str("reason", reasonLabel). | |
| 885 | + | Int("reason_code", int(reason)). | |
| 886 | + | Str("uri", uri). | |
| 887 | + | Msg("web process terminated") | |
| 888 | + | ||
| 889 | + | if wv.OnWebProcessTerminated != nil { | |
| 890 | + | wv.OnWebProcessTerminated(reason, reasonLabel, uri) | |
| 891 | + | } | |
| 892 | + | } | |
| 893 | + | sigID := wv.inner.ConnectWebProcessTerminated(&terminatedCb) | |
| 894 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |
| 861 | 895 | } | |
| 862 | 896 | ||
| 863 | 897 | // ID returns the unique identifier for this WebView. | |
| @@ -1236,13 +1270,13 @@ func (wv *WebView) DisconnectSignals() { | |||
| 1236 | 1270 | ||
| 1237 | 1271 | obj := gobject.ObjectNewFromInternalPtr(wv.inner.GoPointer()) | |
| 1238 | 1272 | for _, sigID := range wv.signalIDs { | |
| 1239 | - | gobject.SignalHandlerDisconnect(obj, sigID) | |
| 1273 | + | gobject.SignalHandlerDisconnect(obj, uint(sigID)) | |
| 1240 | 1274 | } | |
| 1241 | 1275 | wv.signalIDs = wv.signalIDs[:0] // Clear the slice | |
| 1242 | 1276 | ||
| 1243 | 1277 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | |
| 1244 | 1278 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | |
| 1245 | - | gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID) | |
| 1279 | + | gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID)) | |
| 1246 | 1280 | wv.backForwardListSignalID = 0 | |
| 1247 | 1281 | wv.backForwardList = nil | |
| 1248 | 1282 | } | |
| @@ -1275,6 +1309,7 @@ func (wv *WebView) Destroy() { | |||
| 1275 | 1309 | wv.OnLeaveFullscreen = nil | |
| 1276 | 1310 | wv.OnAudioStateChanged = nil | |
| 1277 | 1311 | wv.OnLinkHover = nil | |
| 1312 | + | wv.OnWebProcessTerminated = nil | |
| 1278 | 1313 | ||
| 1279 | 1314 | // 3. Clear async callback references | |
| 1280 | 1315 | wv.mu.Lock() | |
| @@ -138,19 +138,20 @@ type WebView struct { | |||||
| 138 | signalIDs []uint32 | 138 | signalIDs []uint32 | ||
| 139 | 139 | ||||
| 140 | // Callbacks (set by UI layer) | 140 | // Callbacks (set by UI layer) | ||
| 141 | - | OnLoadChanged func(LoadEvent) | 141 | + | OnLoadChanged func(LoadEvent) |
| 142 | - | OnTitleChanged func(string) | 142 | + | OnTitleChanged func(string) |
| 143 | - | OnURIChanged func(string) | 143 | + | OnURIChanged func(string) |
| 144 | - | OnProgressChanged func(float64) | 144 | + | OnProgressChanged func(float64) |
| 145 | - | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes | 145 | + | OnFaviconChanged func(*gdk.Texture) // Called when page favicon changes |
| 146 | - | OnClose func() | 146 | + | OnClose func() |
| 147 | - | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | 147 | + | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup |
| 148 | - | OnReadyToShow func() // Called when popup is ready to display | 148 | + | OnReadyToShow func() // Called when popup is ready to display |
| 149 | - | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) | 149 | + | OnLinkMiddleClick func(uri string) bool // Return true if handled (blocks navigation) |
| 150 | - | OnEnterFullscreen func() bool // Return true to prevent fullscreen | 150 | + | OnEnterFullscreen func() bool // Return true to prevent fullscreen |
| 151 | - | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen | 151 | + | OnLeaveFullscreen func() bool // Return true to prevent leaving fullscreen |
| 152 | - | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | 152 | + | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops |
| 153 | - | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) | 153 | + | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) |
| 154 | + | OnWebProcessTerminated func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) | |||
| 154 | 155 | ||||
| 155 | logger zerolog.Logger | 156 | logger zerolog.Logger | ||
| 156 | mu sync.RWMutex | 157 | mu sync.RWMutex | ||
| @@ -241,7 +242,7 @@ func (a *findControllerAdapter) OnFoundText(callback func(matchCount uint)) uint | |||||
| 241 | cb := func(_ webkit.FindController, matchCount uint) { | 242 | cb := func(_ webkit.FindController, matchCount uint) { | ||
| 242 | callback(matchCount) | 243 | callback(matchCount) | ||
| 243 | } | 244 | } | ||
| 244 | - | return a.fc.ConnectFoundText(&cb) | 245 | + | return uint32(a.fc.ConnectFoundText(&cb)) |
| 245 | } | 246 | } | ||
| 246 | 247 | ||||
| 247 | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | 248 | func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | ||
| @@ -251,7 +252,7 @@ func (a *findControllerAdapter) OnFailedToFindText(callback func()) uint32 { | |||||
| 251 | cb := func(_ webkit.FindController) { | 252 | cb := func(_ webkit.FindController) { | ||
| 252 | callback() | 253 | callback() | ||
| 253 | } | 254 | } | ||
| 254 | - | return a.fc.ConnectFailedToFindText(&cb) | 255 | + | return uint32(a.fc.ConnectFailedToFindText(&cb)) |
| 255 | } | 256 | } | ||
| 256 | 257 | ||||
| 257 | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint32 { | 258 | func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) uint32 { | ||
| @@ -261,7 +262,7 @@ func (a *findControllerAdapter) OnCountedMatches(callback func(matchCount uint)) | |||||
| 261 | cb := func(_ webkit.FindController, matchCount uint) { | 262 | cb := func(_ webkit.FindController, matchCount uint) { | ||
| 262 | callback(matchCount) | 263 | callback(matchCount) | ||
| 263 | } | 264 | } | ||
| 264 | - | return a.fc.ConnectCountedMatches(&cb) | 265 | + | return uint32(a.fc.ConnectCountedMatches(&cb)) |
| 265 | } | 266 | } | ||
| 266 | 267 | ||||
| 267 | func (a *findControllerAdapter) DisconnectSignal(id uint32) { | 268 | func (a *findControllerAdapter) DisconnectSignal(id uint32) { | ||
| @@ -269,7 +270,7 @@ func (a *findControllerAdapter) DisconnectSignal(id uint32) { | |||||
| 269 | return | 270 | return | ||
| 270 | } | 271 | } | ||
| 271 | obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer()) | 272 | obj := gobject.ObjectNewFromInternalPtr(a.fc.GoPointer()) | ||
| 272 | - | gobject.SignalHandlerDisconnect(obj, id) | 273 | + | gobject.SignalHandlerDisconnect(obj, uint(id)) |
| 273 | } | 274 | } | ||
| 274 | 275 | ||||
| 275 | // NewWebView creates a new WebView with the given context and settings. | 276 | // NewWebView creates a new WebView with the given context and settings. | ||
| @@ -388,6 +389,7 @@ func (wv *WebView) connectSignals() { | |||||
| 388 | wv.connectAudioStateSignal() | 389 | wv.connectAudioStateSignal() | ||
| 389 | wv.connectMouseTargetChangedSignal() | 390 | wv.connectMouseTargetChangedSignal() | ||
| 390 | wv.connectBackForwardListChangedSignal() | 391 | wv.connectBackForwardListChangedSignal() | ||
| 392 | + | wv.connectWebProcessTerminatedSignal() | |||
| 391 | } | 393 | } | ||
| 392 | 394 | ||||
| 393 | func (wv *WebView) connectLoadChangedSignal() { | 395 | func (wv *WebView) connectLoadChangedSignal() { | ||
| @@ -421,7 +423,7 @@ func (wv *WebView) connectLoadChangedSignal() { | |||||
| 421 | } | 423 | } | ||
| 422 | } | 424 | } | ||
| 423 | sigID := wv.inner.ConnectLoadChanged(&loadChangedCb) | 425 | sigID := wv.inner.ConnectLoadChanged(&loadChangedCb) | ||
| 424 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 426 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 425 | } | 427 | } | ||
| 426 | 428 | ||||
| 427 | func (wv *WebView) connectCloseSignal() { | 429 | func (wv *WebView) connectCloseSignal() { | ||
| @@ -431,7 +433,7 @@ func (wv *WebView) connectCloseSignal() { | |||||
| 431 | } | 433 | } | ||
| 432 | } | 434 | } | ||
| 433 | sigID := wv.inner.ConnectClose(&closeCb) | 435 | sigID := wv.inner.ConnectClose(&closeCb) | ||
| 434 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 436 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 435 | } | 437 | } | ||
| 436 | 438 | ||||
| 437 | func (wv *WebView) connectCreateSignal() { | 439 | func (wv *WebView) connectCreateSignal() { | ||
| @@ -473,7 +475,7 @@ func (wv *WebView) connectCreateSignal() { | |||||
| 473 | return newWV.inner.Widget | 475 | return newWV.inner.Widget | ||
| 474 | } | 476 | } | ||
| 475 | sigID := wv.inner.ConnectCreate(&createCb) | 477 | sigID := wv.inner.ConnectCreate(&createCb) | ||
| 476 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 478 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 477 | } | 479 | } | ||
| 478 | 480 | ||||
| 479 | func (wv *WebView) connectReadyToShowSignal() { | 481 | func (wv *WebView) connectReadyToShowSignal() { | ||
| @@ -483,7 +485,7 @@ func (wv *WebView) connectReadyToShowSignal() { | |||||
| 483 | } | 485 | } | ||
| 484 | } | 486 | } | ||
| 485 | sigID := wv.inner.ConnectReadyToShow(&readyToShowCb) | 487 | sigID := wv.inner.ConnectReadyToShow(&readyToShowCb) | ||
| 486 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 488 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 487 | } | 489 | } | ||
| 488 | 490 | ||||
| 489 | func (wv *WebView) connectTitleSignal() { | 491 | func (wv *WebView) connectTitleSignal() { | ||
| @@ -498,7 +500,7 @@ func (wv *WebView) connectTitleSignal() { | |||||
| 498 | } | 500 | } | ||
| 499 | } | 501 | } | ||
| 500 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::title", glib.NewCallback(&titleCb)) | 502 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::title", glib.NewCallback(&titleCb)) | ||
| 501 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 503 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 502 | } | 504 | } | ||
| 503 | 505 | ||||
| 504 | func (wv *WebView) connectURISignal() { | 506 | func (wv *WebView) connectURISignal() { | ||
| @@ -514,7 +516,7 @@ func (wv *WebView) connectURISignal() { | |||||
| 514 | } | 516 | } | ||
| 515 | } | 517 | } | ||
| 516 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::uri", glib.NewCallback(&uriCb)) | 518 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::uri", glib.NewCallback(&uriCb)) | ||
| 517 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 519 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 518 | } | 520 | } | ||
| 519 | 521 | ||||
| 520 | func (wv *WebView) connectFaviconSignal() { | 522 | func (wv *WebView) connectFaviconSignal() { | ||
| @@ -525,7 +527,7 @@ func (wv *WebView) connectFaviconSignal() { | |||||
| 525 | } | 527 | } | ||
| 526 | } | 528 | } | ||
| 527 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::favicon", glib.NewCallback(&faviconCb)) | 529 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::favicon", glib.NewCallback(&faviconCb)) | ||
| 528 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 530 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 529 | } | 531 | } | ||
| 530 | 532 | ||||
| 531 | // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead. | 533 | // progressThrottleInterval limits progress callbacks to ~60fps to reduce UI overhead. | ||
| @@ -555,7 +557,7 @@ func (wv *WebView) connectProgressSignal() { | |||||
| 555 | } | 557 | } | ||
| 556 | } | 558 | } | ||
| 557 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::estimated-load-progress", glib.NewCallback(&progressCb)) | 559 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::estimated-load-progress", glib.NewCallback(&progressCb)) | ||
| 558 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 560 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 559 | } | 561 | } | ||
| 560 | 562 | ||||
| 561 | func (wv *WebView) connectDecidePolicySignal() { | 563 | func (wv *WebView) connectDecidePolicySignal() { | ||
| @@ -571,7 +573,7 @@ func (wv *WebView) connectDecidePolicySignal() { | |||||
| 571 | } | 573 | } | ||
| 572 | } | 574 | } | ||
| 573 | sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb) | 575 | sigID := wv.inner.ConnectDecidePolicy(&decidePolicyCb) | ||
| 574 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 576 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 575 | } | 577 | } | ||
| 576 | 578 | ||||
| 577 | // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads). | 579 | // handleResponsePolicyDecision handles response policy decisions (e.g., forcing downloads). | ||
| @@ -770,7 +772,7 @@ func (wv *WebView) connectEnterFullscreenSignal() { | |||||
| 770 | return false // Allow fullscreen | 772 | return false // Allow fullscreen | ||
| 771 | } | 773 | } | ||
| 772 | sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb) | 774 | sigID := wv.inner.ConnectEnterFullscreen(&enterFullscreenCb) | ||
| 773 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 775 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 774 | } | 776 | } | ||
| 775 | 777 | ||||
| 776 | func (wv *WebView) connectLeaveFullscreenSignal() { | 778 | func (wv *WebView) connectLeaveFullscreenSignal() { | ||
| @@ -783,7 +785,7 @@ func (wv *WebView) connectLeaveFullscreenSignal() { | |||||
| 783 | return false // Allow leaving fullscreen | 785 | return false // Allow leaving fullscreen | ||
| 784 | } | 786 | } | ||
| 785 | sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb) | 787 | sigID := wv.inner.ConnectLeaveFullscreen(&leaveFullscreenCb) | ||
| 786 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 788 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 787 | } | 789 | } | ||
| 788 | 790 | ||||
| 789 | func (wv *WebView) connectAudioStateSignal() { | 791 | func (wv *WebView) connectAudioStateSignal() { | ||
| @@ -801,7 +803,7 @@ func (wv *WebView) connectAudioStateSignal() { | |||||
| 801 | } | 803 | } | ||
| 802 | } | 804 | } | ||
| 803 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | 805 | sigID := gobject.SignalConnect(wv.inner.GoPointer(), "notify::is-playing-audio", glib.NewCallback(&audioCb)) | ||
| 804 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 806 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 805 | } | 807 | } | ||
| 806 | 808 | ||||
| 807 | func (wv *WebView) connectMouseTargetChangedSignal() { | 809 | func (wv *WebView) connectMouseTargetChangedSignal() { | ||
| @@ -829,7 +831,7 @@ func (wv *WebView) connectMouseTargetChangedSignal() { | |||||
| 829 | wv.OnLinkHover(uri) | 831 | wv.OnLinkHover(uri) | ||
| 830 | } | 832 | } | ||
| 831 | sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb) | 833 | sigID := wv.inner.ConnectMouseTargetChanged(&mouseTargetCb) | ||
| 832 | - | wv.signalIDs = append(wv.signalIDs, sigID) | 834 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) |
| 833 | } | 835 | } | ||
| 834 | 836 | ||||
| 835 | func (wv *WebView) connectBackForwardListChangedSignal() { | 837 | func (wv *WebView) connectBackForwardListChangedSignal() { | ||
| @@ -857,7 +859,39 @@ func (wv *WebView) connectBackForwardListChangedSignal() { | |||||
| 857 | } | 859 | } | ||
| 858 | sigID := backForwardList.ConnectChanged(&changedCb) | 860 | sigID := backForwardList.ConnectChanged(&changedCb) | ||
| 859 | wv.backForwardList = backForwardList | 861 | wv.backForwardList = backForwardList | ||
| 860 | - | wv.backForwardListSignalID = sigID | 862 | + | wv.backForwardListSignalID = uint32(sigID) |
| 863 | + | } | |||
| 864 | + | ||||
| 865 | + | func webProcessTerminationReasonString(reason webkit.WebProcessTerminationReason) string { | |||
| 866 | + | switch reason { | |||
| 867 | + | case webkit.WebProcessCrashedValue: | |||
| 868 | + | return "crashed" | |||
| 869 | + | case webkit.WebProcessExceededMemoryLimitValue: | |||
| 870 | + | return "exceeded_memory" | |||
| 871 | + | case webkit.WebProcessTerminatedByApiValue: | |||
| 872 | + | return "terminated_by_api" | |||
| 873 | + | default: | |||
| 874 | + | return "unknown" | |||
| 875 | + | } | |||
| 876 | + | } | |||
| 877 | + | ||||
| 878 | + | func (wv *WebView) connectWebProcessTerminatedSignal() { | |||
| 879 | + | terminatedCb := func(_ webkit.WebView, reason webkit.WebProcessTerminationReason) { | |||
| 880 | + | uri := wv.URI() | |||
| 881 | + | reasonLabel := webProcessTerminationReasonString(reason) | |||
| 882 | + | wv.logger.Warn(). | |||
| 883 | + | Uint64("id", uint64(wv.id)). | |||
| 884 | + | Str("reason", reasonLabel). | |||
| 885 | + | Int("reason_code", int(reason)). | |||
| 886 | + | Str("uri", uri). | |||
| 887 | + | Msg("web process terminated") | |||
| 888 | + | ||||
| 889 | + | if wv.OnWebProcessTerminated != nil { | |||
| 890 | + | wv.OnWebProcessTerminated(reason, reasonLabel, uri) | |||
| 891 | + | } | |||
| 892 | + | } | |||
| 893 | + | sigID := wv.inner.ConnectWebProcessTerminated(&terminatedCb) | |||
| 894 | + | wv.signalIDs = append(wv.signalIDs, uint32(sigID)) | |||
| 861 | } | 895 | } | ||
| 862 | 896 | ||||
| 863 | // ID returns the unique identifier for this WebView. | 897 | // ID returns the unique identifier for this WebView. | ||
| @@ -1236,13 +1270,13 @@ func (wv *WebView) DisconnectSignals() { | |||||
| 1236 | 1270 | ||||
| 1237 | obj := gobject.ObjectNewFromInternalPtr(wv.inner.GoPointer()) | 1271 | obj := gobject.ObjectNewFromInternalPtr(wv.inner.GoPointer()) | ||
| 1238 | for _, sigID := range wv.signalIDs { | 1272 | for _, sigID := range wv.signalIDs { | ||
| 1239 | - | gobject.SignalHandlerDisconnect(obj, sigID) | 1273 | + | gobject.SignalHandlerDisconnect(obj, uint(sigID)) |
| 1240 | } | 1274 | } | ||
| 1241 | wv.signalIDs = wv.signalIDs[:0] // Clear the slice | 1275 | wv.signalIDs = wv.signalIDs[:0] // Clear the slice | ||
| 1242 | 1276 | ||||
| 1243 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | 1277 | if wv.backForwardList != nil && wv.backForwardListSignalID != 0 { | ||
| 1244 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | 1278 | bfObj := gobject.ObjectNewFromInternalPtr(wv.backForwardList.GoPointer()) | ||
| 1245 | - | gobject.SignalHandlerDisconnect(bfObj, wv.backForwardListSignalID) | 1279 | + | gobject.SignalHandlerDisconnect(bfObj, uint(wv.backForwardListSignalID)) |
| 1246 | wv.backForwardListSignalID = 0 | 1280 | wv.backForwardListSignalID = 0 | ||
| 1247 | wv.backForwardList = nil | 1281 | wv.backForwardList = nil | ||
| 1248 | } | 1282 | } | ||
| @@ -1275,6 +1309,7 @@ func (wv *WebView) Destroy() { | |||||
| 1275 | wv.OnLeaveFullscreen = nil | 1309 | wv.OnLeaveFullscreen = nil | ||
| 1276 | wv.OnAudioStateChanged = nil | 1310 | wv.OnAudioStateChanged = nil | ||
| 1277 | wv.OnLinkHover = nil | 1311 | wv.OnLinkHover = nil | ||
| 1312 | + | wv.OnWebProcessTerminated = nil | |||
| 1278 | 1313 | ||||
| 1279 | // 3. Clear async callback references | 1314 | // 3. Clear async callback references | ||
| 1280 | wv.mu.Lock() | 1315 | wv.mu.Lock() | ||
A internal/infrastructure/webkit/webview_termination_test.go +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | package webkit | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | webkitlib "github.com/bnema/puregotk-webkit/webkit" | |
| 7 | + | "github.com/stretchr/testify/assert" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestWebProcessTerminationReasonString(t *testing.T) { | |
| 11 | + | assert.Equal(t, "crashed", webProcessTerminationReasonString(webkitlib.WebProcessCrashedValue)) | |
| 12 | + | assert.Equal(t, "exceeded_memory", webProcessTerminationReasonString(webkitlib.WebProcessExceededMemoryLimitValue)) | |
| 13 | + | assert.Equal(t, "terminated_by_api", webProcessTerminationReasonString(webkitlib.WebProcessTerminatedByApiValue)) | |
| 14 | + | assert.Equal(t, "unknown", webProcessTerminationReasonString(webkitlib.WebProcessTerminationReason(99))) | |
| 15 | + | } | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | package webkit | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | webkitlib "github.com/bnema/puregotk-webkit/webkit" | |||
| 7 | + | "github.com/stretchr/testify/assert" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestWebProcessTerminationReasonString(t *testing.T) { | |||
| 11 | + | assert.Equal(t, "crashed", webProcessTerminationReasonString(webkitlib.WebProcessCrashedValue)) | |||
| 12 | + | assert.Equal(t, "exceeded_memory", webProcessTerminationReasonString(webkitlib.WebProcessExceededMemoryLimitValue)) | |||
| 13 | + | assert.Equal(t, "terminated_by_api", webProcessTerminationReasonString(webkitlib.WebProcessTerminatedByApiValue)) | |||
| 14 | + | assert.Equal(t, "unknown", webProcessTerminationReasonString(webkitlib.WebProcessTerminationReason(99))) | |||
| 15 | + | } | |||
M internal/ui/layout/gtk_widgets.go +3 -3
| @@ -163,14 +163,14 @@ func (p *gtkPaned) ConnectMap(callback func()) uint32 { | |||
| 163 | 163 | cb := func(_ gtk.Widget) { | |
| 164 | 164 | callback() | |
| 165 | 165 | } | |
| 166 | - | return p.inner.ConnectMap(&cb) | |
| 166 | + | return uint32(p.inner.ConnectMap(&cb)) | |
| 167 | 167 | } | |
| 168 | 168 | ||
| 169 | 169 | func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint32 { | |
| 170 | 170 | cb := func(_ gobject.Object, _ uintptr) { | |
| 171 | 171 | callback() | |
| 172 | 172 | } | |
| 173 | - | return p.inner.ConnectNotifyWithDetail("position", &cb) | |
| 173 | + | return uint32(p.inner.ConnectNotifyWithDetail("position", &cb)) | |
| 174 | 174 | } | |
| 175 | 175 | ||
| 176 | 176 | func (p *gtkPaned) AddTickCallback(callback func() bool) uint { | |
| @@ -543,7 +543,7 @@ func (btn *gtkButton) ConnectClicked(callback func()) uint32 { | |||
| 543 | 543 | cb := func(_ gtk.Button) { | |
| 544 | 544 | callback() | |
| 545 | 545 | } | |
| 546 | - | return btn.inner.ConnectClicked(&cb) | |
| 546 | + | return uint32(btn.inner.ConnectClicked(&cb)) | |
| 547 | 547 | } | |
| 548 | 548 | ||
| 549 | 549 | func (btn *gtkButton) GetAllocatedWidth() int { return btn.inner.GetAllocatedWidth() } | |
| @@ -163,14 +163,14 @@ func (p *gtkPaned) ConnectMap(callback func()) uint32 { | |||||
| 163 | cb := func(_ gtk.Widget) { | 163 | cb := func(_ gtk.Widget) { | ||
| 164 | callback() | 164 | callback() | ||
| 165 | } | 165 | } | ||
| 166 | - | return p.inner.ConnectMap(&cb) | 166 | + | return uint32(p.inner.ConnectMap(&cb)) |
| 167 | } | 167 | } | ||
| 168 | 168 | ||||
| 169 | func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint32 { | 169 | func (p *gtkPaned) ConnectNotifyPosition(callback func()) uint32 { | ||
| 170 | cb := func(_ gobject.Object, _ uintptr) { | 170 | cb := func(_ gobject.Object, _ uintptr) { | ||
| 171 | callback() | 171 | callback() | ||
| 172 | } | 172 | } | ||
| 173 | - | return p.inner.ConnectNotifyWithDetail("position", &cb) | 173 | + | return uint32(p.inner.ConnectNotifyWithDetail("position", &cb)) |
| 174 | } | 174 | } | ||
| 175 | 175 | ||||
| 176 | func (p *gtkPaned) AddTickCallback(callback func() bool) uint { | 176 | func (p *gtkPaned) AddTickCallback(callback func() bool) uint { | ||
| @@ -543,7 +543,7 @@ func (btn *gtkButton) ConnectClicked(callback func()) uint32 { | |||||
| 543 | cb := func(_ gtk.Button) { | 543 | cb := func(_ gtk.Button) { | ||
| 544 | callback() | 544 | callback() | ||
| 545 | } | 545 | } | ||
| 546 | - | return btn.inner.ConnectClicked(&cb) | 546 | + | return uint32(btn.inner.ConnectClicked(&cb)) |
| 547 | } | 547 | } | ||
| 548 | 548 | ||||
| 549 | func (btn *gtkButton) GetAllocatedWidth() int { return btn.inner.GetAllocatedWidth() } | 549 | func (btn *gtkButton) GetAllocatedWidth() int { return btn.inner.GetAllocatedWidth() } | ||
M internal/ui/layout/stacked.go +1 -1
| @@ -275,7 +275,7 @@ func disconnectButtonSignal(btn ButtonWidget, signalID uint32) { | |||
| 275 | 275 | } | |
| 276 | 276 | ||
| 277 | 277 | obj := gobject.ObjectNewFromInternalPtr(ptr) | |
| 278 | - | gobject.SignalHandlerDisconnect(obj, signalID) | |
| 278 | + | gobject.SignalHandlerDisconnect(obj, uint(signalID)) | |
| 279 | 279 | } | |
| 280 | 280 | ||
| 281 | 281 | // InsertPaneAfter inserts a new pane after the specified index position. | |
| @@ -275,7 +275,7 @@ func disconnectButtonSignal(btn ButtonWidget, signalID uint32) { | |||||
| 275 | } | 275 | } | ||
| 276 | 276 | ||||
| 277 | obj := gobject.ObjectNewFromInternalPtr(ptr) | 277 | obj := gobject.ObjectNewFromInternalPtr(ptr) | ||
| 278 | - | gobject.SignalHandlerDisconnect(obj, signalID) | 278 | + | gobject.SignalHandlerDisconnect(obj, uint(signalID)) |
| 279 | } | 279 | } | ||
| 280 | 280 | ||||
| 281 | // InsertPaneAfter inserts a new pane after the specified index position. | 281 | // InsertPaneAfter inserts a new pane after the specified index position. | ||