summaryrefslogtreecommitdiff
path: root/internal/probemanager
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-13 20:04:48 +0300
committerPaul Buetow <paul@buetow.org>2026-05-13 20:04:48 +0300
commit251894cf3375812564ecf28392179b395cdda9c7 (patch)
tree83c3609ab591702e29a375923670e7622a33b5c7 /internal/probemanager
parent78ea9e22e596255c5e23ce445d80641870674ca9 (diff)
refactor: break down functions exceeding 50 lines into smaller helpers
Split 22 production files across the codebase — event loop, TUI models, probe manager, dashboard, export, flag parsing, code generation, and ioworkload scenarios — so that no function body exceeds 50 lines. Each extracted helper carries its own comment explaining its role. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/probemanager')
-rw-r--r--internal/probemanager/manager.go154
1 files changed, 99 insertions, 55 deletions
diff --git a/internal/probemanager/manager.go b/internal/probemanager/manager.go
index 677762b..8b15f94 100644
--- a/internal/probemanager/manager.go
+++ b/internal/probemanager/manager.go
@@ -138,6 +138,7 @@ func (m *Manager) Toggle(syscall string) error {
}
// Attach attaches enter/exit tracepoints for a registered syscall.
+// Attach attaches enter/exit tracepoints for a registered syscall.
func (m *Manager) Attach(syscall string) error {
if syscall == "" {
return errors.New("syscall is required")
@@ -153,25 +154,47 @@ func (m *Manager) Attach(syscall string) error {
entry.attachMu.Lock()
defer entry.attachMu.Unlock()
+ // Re-acquire the lock after the per-entry mutex to prevent races with
+ // concurrent Detach calls on the same syscall.
+ enterTP, exitTP, attacher, err := m.snapshotAttachParams(syscall, entry)
+ if err != nil {
+ return err
+ }
+ if attacher == nil {
+ return nil // entry was already active
+ }
+
+ enterLink, exitLink, attachErr := attachPair(attacher, enterTP, exitTP)
+ return m.commitAttach(syscall, entry, enterLink, exitLink, attachErr)
+}
+
+// snapshotAttachParams re-validates the entry under the manager lock and
+// returns the tracepoint names and attacher needed for attachPair. It returns
+// (nil attacher, nil error) when the probe is already active.
+func (m *Manager) snapshotAttachParams(syscall string, entry *probeEntry) (enterTP, exitTP string, attacher Attacher, err error) {
m.mu.Lock()
entry, err = m.entryLocked(syscall)
if err != nil {
m.mu.Unlock()
- return err
+ return "", "", nil, err
}
if entry.active {
m.mu.Unlock()
- return nil
+ return "", "", nil, nil
}
- enterTP := entry.enterTP
- exitTP := entry.exitTP
- attacher := m.attacher
+ enterTP = entry.enterTP
+ exitTP = entry.exitTP
+ attacher = m.attacher
m.mu.Unlock()
+ return enterTP, exitTP, attacher, nil
+}
- enterLink, exitLink, attachErr := attachPair(attacher, enterTP, exitTP)
-
+// commitAttach stores the newly attached link pair in entry under the manager
+// lock, recording any attach error or cleaning up on a concurrent manager close.
+func (m *Manager) commitAttach(syscall string, entry *probeEntry, enterLink, exitLink Link, attachErr error) error {
m.mu.Lock()
defer m.mu.Unlock()
+ var err error
entry, err = m.entryLocked(syscall)
if err != nil {
return errors.Join(
@@ -180,13 +203,11 @@ func (m *Manager) Attach(syscall string) error {
destroyLink(fmt.Sprintf("cleanup exit %s", syscall), exitLink),
)
}
-
if attachErr != nil {
entry.lastErr = attachErr
entry.active = entry.enterLink != nil || entry.exitLink != nil
return attachErr
}
-
entry.enterLink = enterLink
entry.exitLink = exitLink
entry.lastErr = nil
@@ -210,6 +231,8 @@ func (m *Manager) Detach(syscall string) error {
entry.attachMu.Lock()
defer entry.attachMu.Unlock()
+ // Re-acquire the lock after the per-entry mutex to prevent races with
+ // concurrent Attach calls on the same syscall.
m.mu.Lock()
entry, err = m.entryLocked(syscall)
if err != nil {
@@ -220,22 +243,31 @@ func (m *Manager) Detach(syscall string) error {
exitLink := entry.exitLink
m.mu.Unlock()
- var errs []string
- enterErr := error(nil)
+ enterErr, exitErr, errs := destroyLinkPair(syscall, enterLink, exitLink)
+ return m.commitDetach(entry, enterErr, exitErr, errs)
+}
+
+// destroyLinkPair destroys both BPF links and collects any errors into a slice.
+// It returns each link's error separately so partial-success can be recorded.
+func destroyLinkPair(syscall string, enterLink, exitLink Link) (enterErr, exitErr error, errs []string) {
if enterLink != nil {
if err := enterLink.Destroy(); err != nil {
enterErr = err
errs = append(errs, fmt.Sprintf("detach enter %s: %v", syscall, err))
}
}
- exitErr := error(nil)
if exitLink != nil {
if err := exitLink.Destroy(); err != nil {
exitErr = err
errs = append(errs, fmt.Sprintf("detach exit %s: %v", syscall, err))
}
}
+ return enterErr, exitErr, errs
+}
+// commitDetach updates entry link pointers and active flag under the manager
+// lock, then returns a combined error if any link destroy failed.
+func (m *Manager) commitDetach(entry *probeEntry, enterErr, exitErr error, errs []string) error {
m.mu.Lock()
defer m.mu.Unlock()
if enterErr == nil {
@@ -313,20 +345,40 @@ func (m *Manager) IsActive(syscall string) bool {
}
// Close detaches all registered probes and marks the manager closed.
+// It returns the first detach error encountered (subsequent errors are
+// recorded on the probe entry but not returned).
func (m *Manager) Close() error {
if m == nil {
return nil
}
+ entries, ok := m.snapshotAndMarkClosed()
+ if !ok {
+ return nil // already closed
+ }
+
+ var firstErr error
+ for _, item := range entries {
+ if err := m.detachProbeEntry(item); err != nil && firstErr == nil {
+ firstErr = err
+ }
+ }
+ return firstErr
+}
+
+// pairEntry groups a probe entry with its syscall name for use during Close.
+type pairEntry struct {
+ syscall string
+ entry *probeEntry
+ hasLinks bool
+}
+// snapshotAndMarkClosed atomically marks the manager as closed and returns a
+// snapshot of all probe entries. Returns (nil, false) if already closed.
+func (m *Manager) snapshotAndMarkClosed() ([]pairEntry, bool) {
m.mu.Lock()
+ defer m.mu.Unlock()
if m.closed {
- m.mu.Unlock()
- return nil
- }
- type pairEntry struct {
- syscall string
- entry *probeEntry
- hasLinks bool
+ return nil, false
}
entries := make([]pairEntry, 0, len(m.probes))
for syscall, entry := range m.probes {
@@ -337,47 +389,39 @@ func (m *Manager) Close() error {
})
}
m.closed = true
- m.mu.Unlock()
+ return entries, true
+}
- var firstErr error
- for _, item := range entries {
- if item.hasLinks {
- item.entry.attachMu.Lock()
- }
- var errForSyscall error
- m.mu.Lock()
- enterLink := item.entry.enterLink
- exitLink := item.entry.exitLink
- item.entry.enterLink = nil
- item.entry.exitLink = nil
- item.entry.active = false
- item.entry.lastErr = nil
- m.mu.Unlock()
+// detachProbeEntry destroys the BPF links for a single probe entry under its
+// per-entry mutex, clears the link pointers, and records any error.
+func (m *Manager) detachProbeEntry(item pairEntry) error {
+ if item.hasLinks {
+ item.entry.attachMu.Lock()
+ defer item.entry.attachMu.Unlock()
+ }
- if enterLink != nil {
- if err := enterLink.Destroy(); err != nil {
- errForSyscall = err
- if firstErr == nil {
- firstErr = err
- }
- }
- }
- if exitLink != nil {
- if err := exitLink.Destroy(); err != nil {
- if errForSyscall == nil {
- errForSyscall = err
- }
- if firstErr == nil {
- firstErr = err
- }
- }
+ m.mu.Lock()
+ enterLink := item.entry.enterLink
+ exitLink := item.entry.exitLink
+ item.entry.enterLink = nil
+ item.entry.exitLink = nil
+ item.entry.active = false
+ item.entry.lastErr = nil
+ m.mu.Unlock()
+
+ var errForSyscall error
+ if enterLink != nil {
+ if err := enterLink.Destroy(); err != nil {
+ errForSyscall = err
}
- m.setLastError(item.syscall, errForSyscall)
- if item.hasLinks {
- item.entry.attachMu.Unlock()
+ }
+ if exitLink != nil {
+ if err := exitLink.Destroy(); err != nil && errForSyscall == nil {
+ errForSyscall = err
}
}
- return firstErr
+ m.setLastError(item.syscall, errForSyscall)
+ return errForSyscall
}
func (m *Manager) entryLocked(syscall string) (*probeEntry, error) {