From c70d61b75a45cea99adcb1f1fe86c0a9f885672e Mon Sep 17 00:00:00 2001 From: Nicolas Palpacuer Date: Thu, 4 Jun 2026 20:19:46 -0400 Subject: [PATCH] Fix stateful-sensor crash on GetStatus; add explicit Status API The stateful-sensor model embedded resource.Named but never initialized it, leaving the embedded interface nil. The sensor service's GetStatus RPC calls the promoted Status method, which dereferenced the nil receiver and crashed the module with a SIGSEGV (exit code 2), producing a restart/crash loop on deployed machines. Initialize the embedded Named via name.AsNamed() (matching the sensor-monitor model) and drop the now-redundant name field and Name() method. Also add an explicit Status method so the model owns the API and returns useful operational metadata (file_path, num_keys) instead of the empty default. Add a regression test covering Status. Co-Authored-By: Claude Opus 4.8 (1M context) --- resources/statefulsensor/statefulsensor.go | 23 +++++++++---- .../statefulsensor/statefulsensor_test.go | 34 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/resources/statefulsensor/statefulsensor.go b/resources/statefulsensor/statefulsensor.go index e3a7665..16c2137 100644 --- a/resources/statefulsensor/statefulsensor.go +++ b/resources/statefulsensor/statefulsensor.go @@ -49,7 +49,6 @@ type statefulSensor struct { resource.AlwaysRebuild resource.Named - name resource.Name logger logging.Logger cfg *Config @@ -91,7 +90,7 @@ func New(ctx context.Context, deps resource.Dependencies, name resource.Name, co } s := &statefulSensor{ - name: name, + Named: name.AsNamed(), logger: logger, cfg: conf, filePath: filePath, @@ -169,10 +168,6 @@ func (s *statefulSensor) saveToFile() error { return nil } -func (s *statefulSensor) Name() resource.Name { - return s.name -} - func (s *statefulSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -185,6 +180,22 @@ func (s *statefulSensor) Readings(ctx context.Context, extra map[string]interfac return readings, nil } +// Status reports operational metadata for the sensor, served via the sensor +// service's GetStatus RPC. It is defined explicitly (rather than relying on the +// empty default promoted from the embedded resource.Named) so the model both +// exposes useful state and is guaranteed to have a non-nil receiver. Unlike +// Readings, which returns the stored value itself, Status reports where the +// value is persisted and how many keys it holds. +func (s *statefulSensor) Status(ctx context.Context) (map[string]interface{}, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + return map[string]interface{}{ + "file_path": s.filePath, + "num_keys": len(s.value), + }, nil +} + // DoCommand supports a "set" command that replaces the value the sensor holds. // // Example: diff --git a/resources/statefulsensor/statefulsensor_test.go b/resources/statefulsensor/statefulsensor_test.go index 504d95d..cf5e048 100644 --- a/resources/statefulsensor/statefulsensor_test.go +++ b/resources/statefulsensor/statefulsensor_test.go @@ -93,6 +93,40 @@ func TestDefaultPathUsesModuleDataDir(t *testing.T) { } } +// TestStatus verifies the explicit Status API. It also guards against a +// regression where the embedded resource.Named was left uninitialized: the +// sensor service's GetStatus RPC calls Status on the resource, and with a nil +// embedded Named the promoted default Status dereferenced a nil receiver and +// crashed the module (SIGSEGV). +func TestStatus(t *testing.T) { + ctx := context.Background() + path := filepath.Join(t.TempDir(), "state.json") + s := newTestSensor(t, path) + + status, err := s.Status(ctx) + if err != nil { + t.Fatalf("Status: %v", err) + } + if status["file_path"] != path { + t.Fatalf("Status file_path = %v, want %q", status["file_path"], path) + } + if status["num_keys"] != 0 { + t.Fatalf("Status num_keys = %v, want 0", status["num_keys"]) + } + + if _, err := s.DoCommand(ctx, map[string]interface{}{"set": map[string]interface{}{"a": 1.0, "b": 2.0}}); err != nil { + t.Fatalf("DoCommand set: %v", err) + } + + status, err = s.Status(ctx) + if err != nil { + t.Fatalf("Status after set: %v", err) + } + if status["num_keys"] != 2 { + t.Fatalf("Status num_keys = %v, want 2", status["num_keys"]) + } +} + func TestDoCommandRejectsUnknownCommand(t *testing.T) { ctx := context.Background() s := newTestSensor(t, filepath.Join(t.TempDir(), "state.json"))