2026-03-23 07:34:16 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
2026-02-19 16:09:11 +00:00
|
|
|
package webview
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-02-23 05:26:39 +00:00
|
|
|
"iter"
|
|
|
|
|
"slices"
|
2026-02-19 16:09:11 +00:00
|
|
|
"sync"
|
2026-03-23 07:34:16 +00:00
|
|
|
"sync/atomic"
|
2026-02-19 16:09:11 +00:00
|
|
|
"time"
|
2026-03-26 13:53:43 +00:00
|
|
|
|
|
|
|
|
core "dappco.re/go/core"
|
2026-02-19 16:09:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ConsoleWatcher provides advanced console message watching capabilities.
|
|
|
|
|
type ConsoleWatcher struct {
|
2026-03-23 07:34:16 +00:00
|
|
|
mu sync.RWMutex
|
|
|
|
|
wv *Webview
|
|
|
|
|
messages []ConsoleMessage
|
|
|
|
|
filters []ConsoleFilter
|
|
|
|
|
limit int
|
|
|
|
|
handlers []consoleHandlerRegistration
|
|
|
|
|
nextHandlerID atomic.Int64
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ConsoleFilter filters console messages.
|
|
|
|
|
type ConsoleFilter struct {
|
|
|
|
|
Type string // Filter by type (log, warn, error, info, debug), empty for all
|
|
|
|
|
Pattern string // Filter by text pattern (substring match)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ConsoleHandler is called when a matching console message is received.
|
|
|
|
|
type ConsoleHandler func(msg ConsoleMessage)
|
|
|
|
|
|
2026-03-23 07:34:16 +00:00
|
|
|
type consoleHandlerRegistration struct {
|
|
|
|
|
id int64
|
|
|
|
|
handler ConsoleHandler
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 16:09:11 +00:00
|
|
|
// NewConsoleWatcher creates a new console watcher for the webview.
|
|
|
|
|
func NewConsoleWatcher(wv *Webview) *ConsoleWatcher {
|
|
|
|
|
cw := &ConsoleWatcher{
|
|
|
|
|
wv: wv,
|
|
|
|
|
messages: make([]ConsoleMessage, 0, 100),
|
|
|
|
|
filters: make([]ConsoleFilter, 0),
|
|
|
|
|
limit: 1000,
|
2026-03-23 07:34:16 +00:00
|
|
|
handlers: make([]consoleHandlerRegistration, 0),
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subscribe to console events from the webview's client
|
|
|
|
|
wv.client.OnEvent("Runtime.consoleAPICalled", func(params map[string]any) {
|
|
|
|
|
cw.handleConsoleEvent(params)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return cw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddFilter adds a filter to the watcher.
|
|
|
|
|
func (cw *ConsoleWatcher) AddFilter(filter ConsoleFilter) {
|
|
|
|
|
cw.mu.Lock()
|
|
|
|
|
defer cw.mu.Unlock()
|
|
|
|
|
cw.filters = append(cw.filters, filter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ClearFilters removes all filters.
|
|
|
|
|
func (cw *ConsoleWatcher) ClearFilters() {
|
|
|
|
|
cw.mu.Lock()
|
|
|
|
|
defer cw.mu.Unlock()
|
|
|
|
|
cw.filters = cw.filters[:0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddHandler adds a handler for console messages.
|
|
|
|
|
func (cw *ConsoleWatcher) AddHandler(handler ConsoleHandler) {
|
2026-03-23 07:34:16 +00:00
|
|
|
cw.addHandler(handler)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (cw *ConsoleWatcher) addHandler(handler ConsoleHandler) int64 {
|
|
|
|
|
cw.mu.Lock()
|
|
|
|
|
defer cw.mu.Unlock()
|
|
|
|
|
id := cw.nextHandlerID.Add(1)
|
|
|
|
|
cw.handlers = append(cw.handlers, consoleHandlerRegistration{
|
|
|
|
|
id: id,
|
|
|
|
|
handler: handler,
|
|
|
|
|
})
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (cw *ConsoleWatcher) removeHandler(id int64) {
|
2026-02-19 16:09:11 +00:00
|
|
|
cw.mu.Lock()
|
|
|
|
|
defer cw.mu.Unlock()
|
2026-03-23 07:34:16 +00:00
|
|
|
|
|
|
|
|
for i, registration := range cw.handlers {
|
|
|
|
|
if registration.id == id {
|
|
|
|
|
cw.handlers = slices.Delete(cw.handlers, i, i+1)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetLimit sets the maximum number of messages to retain.
|
|
|
|
|
func (cw *ConsoleWatcher) SetLimit(limit int) {
|
|
|
|
|
cw.mu.Lock()
|
|
|
|
|
defer cw.mu.Unlock()
|
|
|
|
|
cw.limit = limit
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Messages returns all captured messages.
|
|
|
|
|
func (cw *ConsoleWatcher) Messages() []ConsoleMessage {
|
2026-02-23 05:26:39 +00:00
|
|
|
return slices.Collect(cw.MessagesAll())
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
|
2026-02-23 05:26:39 +00:00
|
|
|
// MessagesAll returns an iterator over all captured messages.
|
|
|
|
|
func (cw *ConsoleWatcher) MessagesAll() iter.Seq[ConsoleMessage] {
|
|
|
|
|
return func(yield func(ConsoleMessage) bool) {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if !yield(msg) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FilteredMessages returns messages matching the current filters.
|
|
|
|
|
func (cw *ConsoleWatcher) FilteredMessages() []ConsoleMessage {
|
2026-02-23 05:26:39 +00:00
|
|
|
return slices.Collect(cw.FilteredMessagesAll())
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
|
2026-02-23 05:26:39 +00:00
|
|
|
// FilteredMessagesAll returns an iterator over messages matching the current filters.
|
|
|
|
|
func (cw *ConsoleWatcher) FilteredMessagesAll() iter.Seq[ConsoleMessage] {
|
|
|
|
|
return func(yield func(ConsoleMessage) bool) {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
2026-02-19 16:09:11 +00:00
|
|
|
|
2026-02-23 05:26:39 +00:00
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if cw.matchesFilter(msg) {
|
|
|
|
|
if !yield(msg) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Errors returns all error messages.
|
|
|
|
|
func (cw *ConsoleWatcher) Errors() []ConsoleMessage {
|
2026-02-23 05:26:39 +00:00
|
|
|
return slices.Collect(cw.ErrorsAll())
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
|
2026-02-23 05:26:39 +00:00
|
|
|
// ErrorsAll returns an iterator over all error messages.
|
|
|
|
|
func (cw *ConsoleWatcher) ErrorsAll() iter.Seq[ConsoleMessage] {
|
|
|
|
|
return func(yield func(ConsoleMessage) bool) {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if msg.Type == "error" {
|
|
|
|
|
if !yield(msg) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Warnings returns all warning messages.
|
|
|
|
|
func (cw *ConsoleWatcher) Warnings() []ConsoleMessage {
|
2026-02-23 05:26:39 +00:00
|
|
|
return slices.Collect(cw.WarningsAll())
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
|
2026-02-23 05:26:39 +00:00
|
|
|
// WarningsAll returns an iterator over all warning messages.
|
|
|
|
|
func (cw *ConsoleWatcher) WarningsAll() iter.Seq[ConsoleMessage] {
|
|
|
|
|
return func(yield func(ConsoleMessage) bool) {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if msg.Type == "warning" {
|
|
|
|
|
if !yield(msg) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear clears all captured messages.
|
|
|
|
|
func (cw *ConsoleWatcher) Clear() {
|
|
|
|
|
cw.mu.Lock()
|
|
|
|
|
defer cw.mu.Unlock()
|
|
|
|
|
cw.messages = cw.messages[:0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WaitForMessage waits for a message matching the filter.
|
|
|
|
|
func (cw *ConsoleWatcher) WaitForMessage(ctx context.Context, filter ConsoleFilter) (*ConsoleMessage, error) {
|
|
|
|
|
// First check existing messages
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if cw.matchesSingleFilter(msg, filter) {
|
|
|
|
|
cw.mu.RUnlock()
|
|
|
|
|
return &msg, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cw.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
// Set up a channel for new messages
|
|
|
|
|
msgCh := make(chan ConsoleMessage, 1)
|
|
|
|
|
handler := func(msg ConsoleMessage) {
|
|
|
|
|
if cw.matchesSingleFilter(msg, filter) {
|
|
|
|
|
select {
|
|
|
|
|
case msgCh <- msg:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 07:34:16 +00:00
|
|
|
handlerID := cw.addHandler(handler)
|
|
|
|
|
defer cw.removeHandler(handlerID)
|
2026-02-19 16:09:11 +00:00
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil, ctx.Err()
|
|
|
|
|
case msg := <-msgCh:
|
|
|
|
|
return &msg, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WaitForError waits for an error message.
|
|
|
|
|
func (cw *ConsoleWatcher) WaitForError(ctx context.Context) (*ConsoleMessage, error) {
|
|
|
|
|
return cw.WaitForMessage(ctx, ConsoleFilter{Type: "error"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HasErrors returns true if there are any error messages.
|
|
|
|
|
func (cw *ConsoleWatcher) HasErrors() bool {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if msg.Type == "error" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Count returns the number of captured messages.
|
|
|
|
|
func (cw *ConsoleWatcher) Count() int {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
|
|
|
|
return len(cw.messages)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ErrorCount returns the number of error messages.
|
|
|
|
|
func (cw *ConsoleWatcher) ErrorCount() int {
|
|
|
|
|
cw.mu.RLock()
|
|
|
|
|
defer cw.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
count := 0
|
|
|
|
|
for _, msg := range cw.messages {
|
|
|
|
|
if msg.Type == "error" {
|
|
|
|
|
count++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleConsoleEvent processes incoming console events.
|
|
|
|
|
func (cw *ConsoleWatcher) handleConsoleEvent(params map[string]any) {
|
|
|
|
|
msgType, _ := params["type"].(string)
|
|
|
|
|
|
|
|
|
|
// Extract args
|
|
|
|
|
args, _ := params["args"].([]any)
|
2026-03-26 13:53:43 +00:00
|
|
|
text := core.NewBuilder()
|
2026-02-19 16:09:11 +00:00
|
|
|
for i, arg := range args {
|
|
|
|
|
if argMap, ok := arg.(map[string]any); ok {
|
|
|
|
|
if val, ok := argMap["value"]; ok {
|
|
|
|
|
if i > 0 {
|
2026-02-22 21:00:17 +00:00
|
|
|
text.WriteString(" ")
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
2026-03-26 13:53:43 +00:00
|
|
|
text.WriteString(core.Sprint(val))
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract stack trace info
|
|
|
|
|
stackTrace, _ := params["stackTrace"].(map[string]any)
|
|
|
|
|
var url string
|
|
|
|
|
var line, column int
|
|
|
|
|
if callFrames, ok := stackTrace["callFrames"].([]any); ok && len(callFrames) > 0 {
|
|
|
|
|
if frame, ok := callFrames[0].(map[string]any); ok {
|
|
|
|
|
url, _ = frame["url"].(string)
|
|
|
|
|
lineFloat, _ := frame["lineNumber"].(float64)
|
|
|
|
|
colFloat, _ := frame["columnNumber"].(float64)
|
|
|
|
|
line = int(lineFloat)
|
|
|
|
|
column = int(colFloat)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msg := ConsoleMessage{
|
|
|
|
|
Type: msgType,
|
2026-02-22 21:00:17 +00:00
|
|
|
Text: text.String(),
|
2026-02-19 16:09:11 +00:00
|
|
|
Timestamp: time.Now(),
|
|
|
|
|
URL: url,
|
|
|
|
|
Line: line,
|
|
|
|
|
Column: column,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cw.addMessage(msg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addMessage adds a message to the store and notifies handlers.
|
|
|
|
|
func (cw *ConsoleWatcher) addMessage(msg ConsoleMessage) {
|
|
|
|
|
cw.mu.Lock()
|
|
|
|
|
|
|
|
|
|
// Enforce limit
|
|
|
|
|
if len(cw.messages) >= cw.limit {
|
fix(console): buffer trim panic when limit < 100, add unit tests
CLAUDE.md: update error wrapping guidance to reflect coreerr.E() convention.
Console buffer trimming in both Webview.addConsoleMessage and
ConsoleWatcher.addMessage panicked with slice bounds out of range
when consoleLimit was smaller than 100. Use min(100, len) for safe
batch trimming.
Added 22 unit tests covering pure functions (FormatConsoleOutput,
containsString, findString, formatJSValue, getString), ConsoleWatcher
filter/count/handler logic, ExceptionWatcher operations, WaitAction
context handling, and buffer limit enforcement. Coverage: 3.2% → 16.1%.
DX audit findings:
- Error handling: clean (all coreerr.E(), no fmt.Errorf)
- File I/O: clean (no os.ReadFile/os.WriteFile — package uses HTTP/WS only)
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 08:54:22 +00:00
|
|
|
drop := min(100, len(cw.messages))
|
|
|
|
|
cw.messages = cw.messages[drop:]
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
cw.messages = append(cw.messages, msg)
|
|
|
|
|
|
|
|
|
|
// Copy handlers to call outside lock
|
2026-02-23 05:26:39 +00:00
|
|
|
handlers := slices.Clone(cw.handlers)
|
2026-02-19 16:09:11 +00:00
|
|
|
cw.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// Call handlers
|
2026-03-23 07:34:16 +00:00
|
|
|
for _, registration := range handlers {
|
|
|
|
|
registration.handler(msg)
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// matchesFilter checks if a message matches any filter.
|
|
|
|
|
func (cw *ConsoleWatcher) matchesFilter(msg ConsoleMessage) bool {
|
|
|
|
|
if len(cw.filters) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
for _, filter := range cw.filters {
|
|
|
|
|
if cw.matchesSingleFilter(msg, filter) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// matchesSingleFilter checks if a message matches a specific filter.
|
|
|
|
|
func (cw *ConsoleWatcher) matchesSingleFilter(msg ConsoleMessage, filter ConsoleFilter) bool {
|
|
|
|
|
if filter.Type != "" && msg.Type != filter.Type {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if filter.Pattern != "" {
|
|
|
|
|
// Simple substring match
|
|
|
|
|
if !containsString(msg.Text, filter.Pattern) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// containsString checks if s contains substr (case-sensitive).
|
|
|
|
|
func containsString(s, substr string) bool {
|
|
|
|
|
return len(substr) == 0 || (len(s) >= len(substr) && findString(s, substr) >= 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// findString finds substr in s, returns -1 if not found.
|
|
|
|
|
func findString(s, substr string) int {
|
2026-02-23 05:26:39 +00:00
|
|
|
for i := range len(s) - len(substr) + 1 {
|
2026-02-19 16:09:11 +00:00
|
|
|
if s[i:i+len(substr)] == substr {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExceptionInfo represents information about a JavaScript exception.
|
|
|
|
|
type ExceptionInfo struct {
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
LineNumber int `json:"lineNumber"`
|
|
|
|
|
ColumnNumber int `json:"columnNumber"`
|
|
|
|
|
URL string `json:"url"`
|
|
|
|
|
StackTrace string `json:"stackTrace"`
|
|
|
|
|
Timestamp time.Time `json:"timestamp"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExceptionWatcher watches for JavaScript exceptions.
|
|
|
|
|
type ExceptionWatcher struct {
|
2026-03-23 07:34:16 +00:00
|
|
|
mu sync.RWMutex
|
|
|
|
|
wv *Webview
|
|
|
|
|
exceptions []ExceptionInfo
|
|
|
|
|
handlers []exceptionHandlerRegistration
|
|
|
|
|
nextHandlerID atomic.Int64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type exceptionHandlerRegistration struct {
|
|
|
|
|
id int64
|
|
|
|
|
handler func(ExceptionInfo)
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewExceptionWatcher creates a new exception watcher.
|
|
|
|
|
func NewExceptionWatcher(wv *Webview) *ExceptionWatcher {
|
|
|
|
|
ew := &ExceptionWatcher{
|
|
|
|
|
wv: wv,
|
|
|
|
|
exceptions: make([]ExceptionInfo, 0),
|
2026-03-23 07:34:16 +00:00
|
|
|
handlers: make([]exceptionHandlerRegistration, 0),
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subscribe to exception events
|
|
|
|
|
wv.client.OnEvent("Runtime.exceptionThrown", func(params map[string]any) {
|
|
|
|
|
ew.handleException(params)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return ew
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exceptions returns all captured exceptions.
|
|
|
|
|
func (ew *ExceptionWatcher) Exceptions() []ExceptionInfo {
|
2026-02-23 05:26:39 +00:00
|
|
|
return slices.Collect(ew.ExceptionsAll())
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
|
2026-02-23 05:26:39 +00:00
|
|
|
// ExceptionsAll returns an iterator over all captured exceptions.
|
|
|
|
|
func (ew *ExceptionWatcher) ExceptionsAll() iter.Seq[ExceptionInfo] {
|
|
|
|
|
return func(yield func(ExceptionInfo) bool) {
|
|
|
|
|
ew.mu.RLock()
|
|
|
|
|
defer ew.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
for _, exc := range ew.exceptions {
|
|
|
|
|
if !yield(exc) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear clears all captured exceptions.
|
|
|
|
|
func (ew *ExceptionWatcher) Clear() {
|
|
|
|
|
ew.mu.Lock()
|
|
|
|
|
defer ew.mu.Unlock()
|
|
|
|
|
ew.exceptions = ew.exceptions[:0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HasExceptions returns true if there are any exceptions.
|
|
|
|
|
func (ew *ExceptionWatcher) HasExceptions() bool {
|
|
|
|
|
ew.mu.RLock()
|
|
|
|
|
defer ew.mu.RUnlock()
|
|
|
|
|
return len(ew.exceptions) > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Count returns the number of exceptions.
|
|
|
|
|
func (ew *ExceptionWatcher) Count() int {
|
|
|
|
|
ew.mu.RLock()
|
|
|
|
|
defer ew.mu.RUnlock()
|
|
|
|
|
return len(ew.exceptions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddHandler adds a handler for exceptions.
|
|
|
|
|
func (ew *ExceptionWatcher) AddHandler(handler func(ExceptionInfo)) {
|
2026-03-23 07:34:16 +00:00
|
|
|
ew.addHandler(handler)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ew *ExceptionWatcher) addHandler(handler func(ExceptionInfo)) int64 {
|
|
|
|
|
ew.mu.Lock()
|
|
|
|
|
defer ew.mu.Unlock()
|
|
|
|
|
id := ew.nextHandlerID.Add(1)
|
|
|
|
|
ew.handlers = append(ew.handlers, exceptionHandlerRegistration{
|
|
|
|
|
id: id,
|
|
|
|
|
handler: handler,
|
|
|
|
|
})
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ew *ExceptionWatcher) removeHandler(id int64) {
|
2026-02-19 16:09:11 +00:00
|
|
|
ew.mu.Lock()
|
|
|
|
|
defer ew.mu.Unlock()
|
2026-03-23 07:34:16 +00:00
|
|
|
|
|
|
|
|
for i, registration := range ew.handlers {
|
|
|
|
|
if registration.id == id {
|
|
|
|
|
ew.handlers = slices.Delete(ew.handlers, i, i+1)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WaitForException waits for an exception to be thrown.
|
|
|
|
|
func (ew *ExceptionWatcher) WaitForException(ctx context.Context) (*ExceptionInfo, error) {
|
|
|
|
|
// Check existing exceptions first
|
|
|
|
|
ew.mu.RLock()
|
|
|
|
|
if len(ew.exceptions) > 0 {
|
|
|
|
|
exc := ew.exceptions[len(ew.exceptions)-1]
|
|
|
|
|
ew.mu.RUnlock()
|
|
|
|
|
return &exc, nil
|
|
|
|
|
}
|
|
|
|
|
ew.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
// Set up a channel for new exceptions
|
|
|
|
|
excCh := make(chan ExceptionInfo, 1)
|
|
|
|
|
handler := func(exc ExceptionInfo) {
|
|
|
|
|
select {
|
|
|
|
|
case excCh <- exc:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 07:34:16 +00:00
|
|
|
handlerID := ew.addHandler(handler)
|
|
|
|
|
defer ew.removeHandler(handlerID)
|
2026-02-19 16:09:11 +00:00
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil, ctx.Err()
|
|
|
|
|
case exc := <-excCh:
|
|
|
|
|
return &exc, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleException processes exception events.
|
|
|
|
|
func (ew *ExceptionWatcher) handleException(params map[string]any) {
|
|
|
|
|
exceptionDetails, ok := params["exceptionDetails"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text, _ := exceptionDetails["text"].(string)
|
|
|
|
|
lineNum, _ := exceptionDetails["lineNumber"].(float64)
|
|
|
|
|
colNum, _ := exceptionDetails["columnNumber"].(float64)
|
|
|
|
|
url, _ := exceptionDetails["url"].(string)
|
|
|
|
|
|
|
|
|
|
// Extract stack trace
|
2026-03-26 13:53:43 +00:00
|
|
|
stackTrace := core.NewBuilder()
|
2026-02-19 16:09:11 +00:00
|
|
|
if st, ok := exceptionDetails["stackTrace"].(map[string]any); ok {
|
|
|
|
|
if frames, ok := st["callFrames"].([]any); ok {
|
|
|
|
|
for _, f := range frames {
|
|
|
|
|
if frame, ok := f.(map[string]any); ok {
|
|
|
|
|
funcName, _ := frame["functionName"].(string)
|
|
|
|
|
frameURL, _ := frame["url"].(string)
|
|
|
|
|
frameLine, _ := frame["lineNumber"].(float64)
|
|
|
|
|
frameCol, _ := frame["columnNumber"].(float64)
|
2026-03-26 13:53:43 +00:00
|
|
|
stackTrace.WriteString(core.Sprintf(" at %s (%s:%d:%d)\n", funcName, frameURL, int(frameLine), int(frameCol)))
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to get exception value description
|
|
|
|
|
if exc, ok := exceptionDetails["exception"].(map[string]any); ok {
|
|
|
|
|
if desc, ok := exc["description"].(string); ok && desc != "" {
|
|
|
|
|
text = desc
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info := ExceptionInfo{
|
|
|
|
|
Text: text,
|
|
|
|
|
LineNumber: int(lineNum),
|
|
|
|
|
ColumnNumber: int(colNum),
|
|
|
|
|
URL: url,
|
2026-02-22 21:00:17 +00:00
|
|
|
StackTrace: stackTrace.String(),
|
2026-02-19 16:09:11 +00:00
|
|
|
Timestamp: time.Now(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ew.mu.Lock()
|
|
|
|
|
ew.exceptions = append(ew.exceptions, info)
|
2026-02-23 05:26:39 +00:00
|
|
|
handlers := slices.Clone(ew.handlers)
|
2026-02-19 16:09:11 +00:00
|
|
|
ew.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// Call handlers
|
2026-03-23 07:34:16 +00:00
|
|
|
for _, registration := range handlers {
|
|
|
|
|
registration.handler(info)
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormatConsoleOutput formats console messages for display.
|
|
|
|
|
func FormatConsoleOutput(messages []ConsoleMessage) string {
|
2026-03-26 13:53:43 +00:00
|
|
|
output := core.NewBuilder()
|
2026-02-19 16:09:11 +00:00
|
|
|
for _, msg := range messages {
|
|
|
|
|
prefix := ""
|
|
|
|
|
switch msg.Type {
|
|
|
|
|
case "error":
|
|
|
|
|
prefix = "[ERROR]"
|
|
|
|
|
case "warning":
|
|
|
|
|
prefix = "[WARN]"
|
|
|
|
|
case "info":
|
|
|
|
|
prefix = "[INFO]"
|
|
|
|
|
case "debug":
|
|
|
|
|
prefix = "[DEBUG]"
|
|
|
|
|
default:
|
|
|
|
|
prefix = "[LOG]"
|
|
|
|
|
}
|
|
|
|
|
timestamp := msg.Timestamp.Format("15:04:05.000")
|
2026-03-26 13:53:43 +00:00
|
|
|
output.WriteString(core.Sprintf("%s %s %s\n", timestamp, prefix, msg.Text))
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|
2026-02-22 21:00:17 +00:00
|
|
|
return output.String()
|
2026-02-19 16:09:11 +00:00
|
|
|
}
|