summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-12 22:53:15 +0300
committerPaul Buetow <paul@buetow.org>2026-05-12 22:53:15 +0300
commit9f8096551ecf7184693b786a8e0b77d290086eac (patch)
tree3e7e1294655202c7339b3bd62cbcbefdfb4ea397
parent35df301fceabfadc8b8a4ae221cc0c2391e233cd (diff)
refactor dispatchRun/validateRunConfig into ModeRegistry for OCP compliance
Introduce a ModeRegistry pattern so new execution modes can be added without modifying the dispatch or validation switch/if chains. Each mode is a self-contained modeHandler (match + validate + run); the ordered defaultRegistry replaces the open if-chains in dispatchRun and validateRunConfig, which become thin wrappers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--internal/ior.go68
-rw-r--r--internal/ior_mode_registry.go216
2 files changed, 227 insertions, 57 deletions
diff --git a/internal/ior.go b/internal/ior.go
index aff1355..f1b159d 100644
--- a/internal/ior.go
+++ b/internal/ior.go
@@ -69,67 +69,18 @@ func Run(cfg flags.Config) error {
return dispatchRun(cfg)
}
+// dispatchRun delegates to the defaultRegistry, which validates all
+// mode-combination constraints and then runs the first matching handler.
func dispatchRun(cfg flags.Config) error {
- if err := validateRunConfig(cfg); err != nil {
- return err
- }
- if cfg.TestFlames {
- return runTUITestFlamesFn(cfg, tuiTestFlamesStarter(cfg))
- }
- if cfg.TestLiveFlames {
- return runTUITestLiveFlamesFn(cfg, tuiTestLiveFlamesStarter(cfg))
- }
- // All remaining modes require tracing, which needs root. Fail fast here so
- // the TUI never starts (and hangs) when we already know it cannot trace.
- if getEUID() != 0 {
- return errRootPrivilegesRequired
- }
- if isHeadlessParquetMode(cfg) {
- return runParquetFn(cfg)
- }
- if shouldRunTraceMode(cfg) {
- return runTraceFn(cfg)
- }
- return runTUIFn(cfg, tuiTraceStarterFromRunTrace(cfg, runTraceWithContextFn))
+ return defaultRegistry.dispatch(cfg)
}
+// validateRunConfig runs all cross-mode constraint checks without running
+// any mode. It is a thin wrapper around defaultRegistry.validate so that
+// callers (and tests) that only want validation do not need to know about
+// the registry.
func validateRunConfig(cfg flags.Config) error {
- if isHeadlessParquetMode(cfg) {
- if cfg.TestFlames {
- return errors.New("--testflames cannot be combined with -parquet")
- }
- if cfg.TestLiveFlames {
- return errors.New("--testliveflames cannot be combined with -parquet")
- }
- if cfg.PlainMode {
- return errors.New("-parquet and -plain are mutually exclusive")
- }
- if cfg.FlamegraphOutput {
- return errors.New("-parquet and -flamegraph are mutually exclusive")
- }
- if hasHeadlessParquetContentFilters(cfg) {
- return errors.New("-parquet cannot be combined with content filters (-comm, -path, -pid, -tid)")
- }
- }
- if cfg.TestFlames && cfg.PlainMode {
- return errors.New("--testflames cannot be combined with -plain")
- }
- if cfg.TestFlames && cfg.FlamegraphOutput {
- return errors.New("--testflames cannot be combined with -flamegraph")
- }
- if cfg.TestLiveFlames && cfg.PlainMode {
- return errors.New("--testliveflames cannot be combined with -plain")
- }
- if cfg.TestLiveFlames && cfg.FlamegraphOutput {
- return errors.New("--testliveflames cannot be combined with -flamegraph")
- }
- if cfg.PlainMode && cfg.FlamegraphOutput {
- return errors.New("-plain and -flamegraph are mutually exclusive")
- }
- if cfg.TestFlames && cfg.TestLiveFlames {
- return errors.New("--testflames and --testliveflames are mutually exclusive")
- }
- return nil
+ return defaultRegistry.validate(cfg)
}
// tuiTestFlamesStarter returns a TraceStarter that seeds static test flame data
@@ -208,6 +159,9 @@ func runSyntheticLiveFlames(ctx context.Context, liveTrie *flamegraph.LiveTrie,
}
}
+// shouldRunTraceMode reports whether cfg selects a headless trace path
+// (plain CSV, flamegraph output, or headless Parquet). It is retained for
+// use by the test suite; the dispatch path uses modeRegistry instead.
func shouldRunTraceMode(cfg flags.Config) bool {
return cfg.PlainMode || cfg.FlamegraphOutput || isHeadlessParquetMode(cfg)
}
diff --git a/internal/ior_mode_registry.go b/internal/ior_mode_registry.go
new file mode 100644
index 0000000..6d04052
--- /dev/null
+++ b/internal/ior_mode_registry.go
@@ -0,0 +1,216 @@
+package internal
+
+import (
+ "errors"
+
+ "ior/internal/flags"
+)
+
+// modeHandler describes a single execution mode for the ior binary.
+// Each mode knows how to recognise itself (match), enforce its
+// invariants (validate), and run (run). The registry evaluates
+// handlers in registration order — the first matching handler wins.
+type modeHandler interface {
+ // match returns true when this handler should own the run.
+ match(cfg flags.Config) bool
+ // validate returns an error if the config is invalid for this mode.
+ // It is called only after the root-privilege check has been skipped
+ // (pre-root modes are checked first and return early before requiring root).
+ validate(cfg flags.Config) error
+ // run executes the mode using the supplied config.
+ run(cfg flags.Config) error
+}
+
+// modeRegistry is an ordered list of modeHandlers.
+// dispatchRun and validateRunConfig iterate through it.
+type modeRegistry []modeHandler
+
+// defaultRegistry is the canonical ordered registry used at runtime.
+// Modes are evaluated first-match-wins, so more specific modes (e.g.,
+// testFlames) are registered before more general ones (e.g., TUI default).
+var defaultRegistry = modeRegistry{
+ &testFlamesModeHandler{},
+ &testLiveFlamesModeHandler{},
+ &headlessParquetModeHandler{},
+ &plainTraceModeHandler{},
+ &tuiModeHandler{},
+}
+
+// dispatch validates cross-mode constraints, requires root when necessary,
+// then delegates to the first matching handler in the registry.
+func (reg modeRegistry) dispatch(cfg flags.Config) error {
+ if err := reg.validate(cfg); err != nil {
+ return err
+ }
+ for _, h := range reg {
+ if h.match(cfg) {
+ return h.run(cfg)
+ }
+ }
+ // Registry must always include a catch-all (tuiModeHandler matches everything).
+ return errors.New("internal: no mode handler matched — this is a bug")
+}
+
+// validate runs cross-mode constraint checks across all handlers in the
+// registry. Handlers that do not match are still checked for rejected
+// combination errors (e.g., parquet + plain is rejected regardless of which
+// handler ultimately runs).
+func (reg modeRegistry) validate(cfg flags.Config) error {
+ for _, h := range reg {
+ if err := h.validate(cfg); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// --- testFlamesModeHandler ---
+
+// testFlamesModeHandler runs the TUI seeded with static synthetic flame data
+// so the flamegraph tab can be exercised without a live BPF trace.
+type testFlamesModeHandler struct{}
+
+func (h *testFlamesModeHandler) match(cfg flags.Config) bool {
+ return cfg.TestFlames
+}
+
+func (h *testFlamesModeHandler) validate(cfg flags.Config) error {
+ if !cfg.TestFlames {
+ return nil
+ }
+ if cfg.PlainMode {
+ return errors.New("--testflames cannot be combined with -plain")
+ }
+ if cfg.FlamegraphOutput {
+ return errors.New("--testflames cannot be combined with -flamegraph")
+ }
+ if isHeadlessParquetMode(cfg) {
+ return errors.New("--testflames cannot be combined with -parquet")
+ }
+ if cfg.TestLiveFlames {
+ return errors.New("--testflames and --testliveflames are mutually exclusive")
+ }
+ return nil
+}
+
+func (h *testFlamesModeHandler) run(cfg flags.Config) error {
+ return runTUITestFlamesFn(cfg, tuiTestFlamesStarter(cfg))
+}
+
+// --- testLiveFlamesModeHandler ---
+
+// testLiveFlamesModeHandler runs the TUI fed by a goroutine that continuously
+// updates a synthetic live-flame trie so the flamegraph tab animates without
+// requiring a real BPF trace.
+type testLiveFlamesModeHandler struct{}
+
+func (h *testLiveFlamesModeHandler) match(cfg flags.Config) bool {
+ return cfg.TestLiveFlames
+}
+
+func (h *testLiveFlamesModeHandler) validate(cfg flags.Config) error {
+ if !cfg.TestLiveFlames {
+ return nil
+ }
+ if cfg.PlainMode {
+ return errors.New("--testliveflames cannot be combined with -plain")
+ }
+ if cfg.FlamegraphOutput {
+ return errors.New("--testliveflames cannot be combined with -flamegraph")
+ }
+ // No need to check parquet+testliveflames here — parquet handler validates
+ // the reverse (parquet cannot be combined with testliveflames).
+ return nil
+}
+
+func (h *testLiveFlamesModeHandler) run(cfg flags.Config) error {
+ return runTUITestLiveFlamesFn(cfg, tuiTestLiveFlamesStarter(cfg))
+}
+
+// --- headlessParquetModeHandler ---
+
+// headlessParquetModeHandler streams all traced syscall events directly to a
+// Parquet file without starting the TUI.
+type headlessParquetModeHandler struct{}
+
+func (h *headlessParquetModeHandler) match(cfg flags.Config) bool {
+ return isHeadlessParquetMode(cfg)
+}
+
+func (h *headlessParquetModeHandler) validate(cfg flags.Config) error {
+ if !isHeadlessParquetMode(cfg) {
+ return nil
+ }
+ // Test-flame modes cannot be combined with headless Parquet because both
+ // require exclusive control over the event source.
+ if cfg.TestFlames {
+ return errors.New("--testflames cannot be combined with -parquet")
+ }
+ if cfg.TestLiveFlames {
+ return errors.New("--testliveflames cannot be combined with -parquet")
+ }
+ if cfg.PlainMode {
+ return errors.New("-parquet and -plain are mutually exclusive")
+ }
+ if cfg.FlamegraphOutput {
+ return errors.New("-parquet and -flamegraph are mutually exclusive")
+ }
+ if hasHeadlessParquetContentFilters(cfg) {
+ return errors.New("-parquet cannot be combined with content filters (-comm, -path, -pid, -tid)")
+ }
+ return nil
+}
+
+func (h *headlessParquetModeHandler) run(cfg flags.Config) error {
+ if getEUID() != 0 {
+ return errRootPrivilegesRequired
+ }
+ return runParquetFn(cfg)
+}
+
+// --- plainTraceModeHandler ---
+
+// plainTraceModeHandler runs a headless BPF trace that writes CSV rows to
+// stdout (plain mode) or a compressed flamegraph file (-flamegraph), without
+// starting the TUI.
+type plainTraceModeHandler struct{}
+
+func (h *plainTraceModeHandler) match(cfg flags.Config) bool {
+ return cfg.PlainMode || cfg.FlamegraphOutput
+}
+
+func (h *plainTraceModeHandler) validate(cfg flags.Config) error {
+ if cfg.PlainMode && cfg.FlamegraphOutput {
+ return errors.New("-plain and -flamegraph are mutually exclusive")
+ }
+ return nil
+}
+
+func (h *plainTraceModeHandler) run(cfg flags.Config) error {
+ if getEUID() != 0 {
+ return errRootPrivilegesRequired
+ }
+ return runTraceFn(cfg)
+}
+
+// --- tuiModeHandler ---
+
+// tuiModeHandler is the catch-all default that launches the full interactive
+// TUI dashboard backed by a live BPF trace. It matches every config that no
+// earlier handler claimed.
+type tuiModeHandler struct{}
+
+func (h *tuiModeHandler) match(_ flags.Config) bool {
+ return true // catch-all default
+}
+
+func (h *tuiModeHandler) validate(_ flags.Config) error {
+ return nil
+}
+
+func (h *tuiModeHandler) run(cfg flags.Config) error {
+ if getEUID() != 0 {
+ return errRootPrivilegesRequired
+ }
+ return runTUIFn(cfg, tuiTraceStarterFromRunTrace(cfg, runTraceWithContextFn))
+}