diff options
| -rw-r--r-- | cmd/ior/main.go | 5 | ||||
| -rw-r--r-- | internal/ior.go | 50 | ||||
| -rw-r--r-- | internal/tui/tui.go | 93 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 41 |
4 files changed, 41 insertions, 148 deletions
diff --git a/cmd/ior/main.go b/cmd/ior/main.go index 2b852f0..8d93d28 100644 --- a/cmd/ior/main.go +++ b/cmd/ior/main.go @@ -23,8 +23,9 @@ func main() { os.Exit(2) } - // Run the internal logic of the application - if err := internal.Run(); err != nil { + // Run the internal logic of the application. + // flags.Get() is called here at the CLI boundary so internal code never reads the singleton. + if err := internal.Run(flags.Get()); err != nil { fmt.Printf("Failed to run: %v\n", err) os.Exit(2) } diff --git a/internal/ior.go b/internal/ior.go index d88f0e3..dba90c3 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -1,7 +1,5 @@ package internal -import "C" - import ( "context" "errors" @@ -38,7 +36,7 @@ var ( runTraceWithContextFn = runTraceWithContext runTUIFn = tui.RunWithTraceStarterConfig runTUITestFlamesFn = tui.RunTestFlamesWithTraceStarterConfig - runTUITestLiveFlamesFn = tui.RunTestFlamesWithTraceStarterConfig + runTUITestLiveFlamesFn = tui.RunTestFlamesWithTraceStarterConfig // same runner; starter differs (static vs live) getEUID = os.Geteuid errRootPrivilegesRequired = errors.New("tracing requires root privileges (run with sudo)") @@ -70,9 +68,10 @@ func (m libbpfTracepointModule) GetProgram(progName string) (probemanager.Progra } // Run is the main entry point for the ior binary. -func Run() error { +// cfg must be provided by the caller; it should not be fetched from the global singleton here. +func Run(cfg flags.Config) error { flags.PrintVersion() - return dispatchRun(flags.Get()) + return dispatchRun(cfg) } func dispatchRun(cfg flags.Config) error { @@ -303,21 +302,14 @@ func applyTraceScopeFromGlobalFilter(cfg *flags.Config, filter globalfilter.Filt } cfg.PidFilter = -1 cfg.TidFilter = -1 - if pid, ok := eqFilterValue(filter.PID); ok { + if pid, ok := filter.PID.EqValue(); ok { cfg.PidFilter = pid } - if tid, ok := eqFilterValue(filter.TID); ok { + if tid, ok := filter.TID.EqValue(); ok { cfg.TidFilter = tid } } -func eqFilterValue(filter *globalfilter.NumericFilter) (int, bool) { - if filter == nil || filter.Op != globalfilter.OpEq || filter.Value <= 0 { - return 0, false - } - return int(filter.Value), true -} - func runTrace(cfg flags.Config) error { return runTraceWithContext(context.Background(), cfg, nil, nil) } @@ -398,24 +390,9 @@ func newEventLoopConfig(cfg flags.Config) eventLoopConfig { } } +// traceFilterFromConfig delegates to the canonical Config.TraceFilter method. func traceFilterFromConfig(cfg flags.Config) globalfilter.Filter { - filter := cfg.GlobalFilter.Clone() - if filter.IsActive() { - return filter - } - if cfg.CommFilter != "" { - filter.Comm = &globalfilter.StringFilter{Pattern: cfg.CommFilter} - } - if cfg.PathFilter != "" { - filter.File = &globalfilter.StringFilter{Pattern: cfg.PathFilter} - } - if cfg.PidFilter > 0 { - filter.PID = &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(cfg.PidFilter)} - } - if cfg.TidFilter > 0 { - filter.TID = &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(cfg.TidFilter)} - } - return filter + return cfg.TraceFilter() } type profilingControl struct { @@ -730,16 +707,9 @@ func headlessParquetTraceConfig(cfg flags.Config) flags.Config { return out } +// parquetMetadata delegates to the canonical parquet.NewFileMetadata. func parquetMetadata(mode string) parquet.FileMetadata { - meta := parquet.FileMetadata{ - StartedAtUnixNano: uint64(time.Now().UnixNano()), - Mode: mode, - IORVersion: flags.Version, - } - if hostname, err := os.Hostname(); err == nil { - meta.Hostname = hostname - } - return meta + return parquet.NewFileMetadata(mode) } type headlessParquetSink struct { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index ef94748..432fc14 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log" - "os" "strconv" "strings" "sync" @@ -231,16 +230,6 @@ func TraceFiltersFromContext(ctx context.Context) (globalfilter.Filter, bool) { return filters.filter.Clone(), true } -// Run starts the TUI program in alternate screen mode. -func Run() error { - return RunWithTraceStarter(defaultTraceStarter) -} - -// RunWithTraceStarter starts the TUI program with a custom trace starter. -func RunWithTraceStarter(starter TraceStarter) error { - return RunWithTraceStarterConfig(flags.Get(), starter) -} - // RunWithTraceStarterConfig starts the TUI with explicit runtime flags. func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { model := newModelWithRuntimeConfig(cfg.PidFilter, filterFromConfig(cfg), cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, starter) @@ -249,12 +238,6 @@ func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { return err } -// RunTestFlamesWithTraceStarter starts the TUI directly on dashboard/flame view -// with a synthetic static flamegraph source. -func RunTestFlamesWithTraceStarter(starter TraceStarter) error { - return RunTestFlamesWithTraceStarterConfig(flags.Get(), starter) -} - // RunTestFlamesWithTraceStarterConfig starts test-flames mode with explicit runtime flags. func RunTestFlamesWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { model := newModelWithRuntimeConfig(1, filterFromConfig(cfg), 1, -1, cfg.TUIExportEnable, starter) @@ -317,9 +300,10 @@ type pickerReturnState struct { tidFilter int } -// NewModel creates the top-level TUI model. +// NewModel creates the top-level TUI model with default runtime flags. +// Prefer NewModelWithConfig to pass parsed CLI config explicitly. func NewModel(initialPID int, startTrace TraceStarter) Model { - return NewModelWithConfig(flags.Get(), initialPID, startTrace) + return NewModelWithConfig(flags.NewFlags(), initialPID, startTrace) } // NewModelWithConfig creates the top-level TUI model with explicit runtime flags. @@ -500,17 +484,6 @@ func (m Model) canHandleDashboardShortcut(msg tea.KeyPressMsg) bool { !m.dashboard.BlocksGlobalShortcuts(msg) } -func (m Model) canQuitFromMainDashboard(msg tea.KeyPressMsg) bool { - return m.screen == ScreenDashboard && - !m.attaching && - m.lastErr == nil && - !m.filterModal.Visible() && - !m.exporter.Visible() && - !m.recordModal.Visible() && - !m.probeModal.Visible() && - !m.dashboard.BlocksGlobalShortcuts(msg) -} - func (m Model) shouldCancelPickerToDashboard(msg tea.KeyPressMsg) bool { return m.screen == ScreenPIDPicker && m.pickerReturn != nil && @@ -537,7 +510,7 @@ func (m Model) handleGlobalKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd, bo return next, cmd, true } if key.Matches(msg, m.keys.Quit) { - if m.canQuitFromMainDashboard(msg) { + if m.canHandleDashboardShortcut(msg) { if err := m.stopRecording(); err != nil { m.lastErr = err return m, nil, true @@ -848,31 +821,9 @@ func defaultTraceStarter(context.Context) error { return nil } +// filterFromConfig delegates to the canonical Config.TraceFilter method. func filterFromConfig(cfg flags.Config) globalfilter.Filter { - filter := cfg.GlobalFilter.Clone() - if filter.IsActive() { - return filter - } - if cfg.CommFilter != "" { - filter.Comm = &globalfilter.StringFilter{Pattern: cfg.CommFilter} - } - if cfg.PathFilter != "" { - filter.File = &globalfilter.StringFilter{Pattern: cfg.PathFilter} - } - if cfg.PidFilter > 0 { - filter.PID = eqNumericFilter(cfg.PidFilter) - } - if cfg.TidFilter > 0 { - filter.TID = eqNumericFilter(cfg.TidFilter) - } - return filter -} - -func eqNumericFilter(value int) *globalfilter.NumericFilter { - if value <= 0 { - return nil - } - return &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(value)} + return cfg.TraceFilter() } func (m *Model) setProcessFilters(pid, tid int) { @@ -887,10 +838,12 @@ func (m *Model) setProcessFilters(pid, tid int) { func (m *Model) setGlobalFilter(filter globalfilter.Filter) { m.globalFilter = filter.Clone() - pid, _ := eqNumericFilterValue(m.globalFilter.PID) - tid, _ := eqNumericFilterValue(m.globalFilter.TID) - m.pidFilter = pid - m.tidFilter = tid + // EqValue returns (0, false) when no equality filter is set; + // selectedPIDFilter maps non-positive values to -1 ("no filter"). + pid, _ := m.globalFilter.PID.EqValue() + tid, _ := m.globalFilter.TID.EqValue() + m.pidFilter = selectedPIDFilter(pid) + m.tidFilter = selectedPIDFilter(tid) m.syncDashboardFilterState() } @@ -903,8 +856,8 @@ func (m *Model) syncDashboardFilterState() { func applyProcessFilters(filter globalfilter.Filter, pid, tid int) globalfilter.Filter { out := filter.Clone() - out.PID = eqNumericFilter(pid) - out.TID = eqNumericFilter(tid) + out.PID = globalfilter.NewEqFilter(int64(pid)) + out.TID = globalfilter.NewEqFilter(int64(tid)) return out } @@ -950,13 +903,6 @@ func (m Model) undoGlobalFilter() (tea.Model, tea.Cmd) { return m, tea.Batch(m.spin.Tick, m.beginTraceCmd()) } -func eqNumericFilterValue(filter *globalfilter.NumericFilter) (int, bool) { - if filter == nil || filter.Op != globalfilter.OpEq || filter.Value <= 0 { - return -1, false - } - return int(filter.Value), true -} - func globalFilterActionLabel(prev, next globalfilter.Filter, action string) string { if strings.TrimSpace(action) != "" { return action @@ -1196,16 +1142,9 @@ func defaultParquetRecordingFilename() string { return fmt.Sprintf("ior-recording-%s.parquet", time.Now().Format("20060102-150405")) } +// tuiParquetMetadata delegates to the canonical parquet.NewFileMetadata. func tuiParquetMetadata() parquet.FileMetadata { - meta := parquet.FileMetadata{ - StartedAtUnixNano: uint64(time.Now().UnixNano()), - Mode: "tui", - IORVersion: flags.Version, - } - if hostname, err := os.Hostname(); err == nil { - meta.Hostname = hostname - } - return meta + return parquet.NewFileMetadata("tui") } func shortenRecordingPath(path string) string { diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 6ce16e6..7f43961 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -88,8 +88,6 @@ func TestRuntimeBindingsContextRoundTrip(t *testing.T) { } func TestPidSelectedTransitionsToDashboardAndSetsPIDFilter(t *testing.T) { - flags.SetPidFilter(-1) - flags.SetTidFilter(99) m := NewModel(-1, func(context.Context) error { return nil }) next, cmd := m.Update(PidSelectedMsg{Pid: 42}) @@ -113,7 +111,6 @@ func TestPidSelectedTransitionsToDashboardAndSetsPIDFilter(t *testing.T) { } func TestInitialPIDSkipsPickerAndStartsTracing(t *testing.T) { - flags.SetPidFilter(-1) m := NewModel(7, func(context.Context) error { return nil }) if m.screen != ScreenDashboard { @@ -127,7 +124,6 @@ func TestInitialPIDSkipsPickerAndStartsTracing(t *testing.T) { } func TestPidSelectedAllSetsNoFilter(t *testing.T) { - flags.SetPidFilter(999) m := NewModel(-1, func(context.Context) error { return nil }) next, _ := m.Update(PidSelectedMsg{Pid: 0}) @@ -696,9 +692,6 @@ func TestTracingStartedAppliesViewportWhenModelSizeIsUnset(t *testing.T) { } func TestExportKeyOpensModalOnDashboard(t *testing.T) { - flags.SetTUIExportEnable(true) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) - m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false @@ -1118,8 +1111,6 @@ func TestPidSelectedClearsPersistentStreamBuffer(t *testing.T) { } func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) { - flags.SetPidFilter(-1) - flags.SetTidFilter(-1) m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false @@ -1146,9 +1137,9 @@ func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) { } func TestSelectTIDKeyReturnsToPickerWhenSinglePIDSelected(t *testing.T) { - flags.SetPidFilter(1234) - flags.SetTidFilter(-1) - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.PidFilter = 1234 + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false m.width = 120 @@ -1174,9 +1165,9 @@ func TestSelectTIDKeyReturnsToPickerWhenSinglePIDSelected(t *testing.T) { } func TestTidSelectedTransitionsToDashboardAndSetsTIDFilter(t *testing.T) { - flags.SetPidFilter(2222) - flags.SetTidFilter(-1) - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.PidFilter = 2222 + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) next, cmd := m.Update(TidSelectedMsg{Pid: 0, Tid: 3333}) if cmd == nil { @@ -1198,8 +1189,6 @@ func TestTidSelectedTransitionsToDashboardAndSetsTIDFilter(t *testing.T) { } func TestTidSelectedFromAllPIDModeSetsOwningPID(t *testing.T) { - flags.SetPidFilter(-1) - flags.SetTidFilter(-1) m := NewModel(-1, func(context.Context) error { return nil }) next, cmd := m.Update(TidSelectedMsg{Pid: 4444, Tid: 5555}) @@ -1219,10 +1208,9 @@ func TestTidSelectedFromAllPIDModeSetsOwningPID(t *testing.T) { } func TestExportKeyIgnoredWhenExportDisabled(t *testing.T) { - flags.SetTUIExportEnable(false) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) - - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.TUIExportEnable = false + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.attaching = false @@ -1234,8 +1222,6 @@ func TestExportKeyIgnoredWhenExportDisabled(t *testing.T) { } func TestStreamFilterModalConsumesEKeyInsteadOfOpeningExport(t *testing.T) { - flags.SetTUIExportEnable(true) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard @@ -1925,8 +1911,6 @@ func advanceFlameSelection(t *testing.T, m *Model) string { } func TestQuestionMarkDoesNotBreakExportModalInput(t *testing.T) { - flags.SetTUIExportEnable(true) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard @@ -1951,10 +1935,9 @@ func TestQuestionMarkDoesNotBreakExportModalInput(t *testing.T) { } func TestStatusBarHidesExportBindingWhenExportDisabled(t *testing.T) { - flags.SetTUIExportEnable(false) - t.Cleanup(func() { flags.SetTUIExportEnable(true) }) - - m := NewModel(-1, func(context.Context) error { return nil }) + cfg := flags.NewFlags() + cfg.TUIExportEnable = false + m := NewModelWithConfig(cfg, -1, func(context.Context) error { return nil }) m.screen = ScreenDashboard m.width = 100 m.height = 30 |
