diff --git a/pkg/coredeno/coredeno.go b/pkg/coredeno/coredeno.go index 8055087..a45bbe5 100644 --- a/pkg/coredeno/coredeno.go +++ b/pkg/coredeno/coredeno.go @@ -58,6 +58,7 @@ type Sidecar struct { cmd *exec.Cmd ctx context.Context cancel context.CancelFunc + done chan struct{} } // NewSidecar creates a Sidecar with the given options. diff --git a/pkg/coredeno/lifecycle.go b/pkg/coredeno/lifecycle.go new file mode 100644 index 0000000..61d5a7c --- /dev/null +++ b/pkg/coredeno/lifecycle.go @@ -0,0 +1,69 @@ +package coredeno + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// Start launches the Deno sidecar process with the given entrypoint args. +func (s *Sidecar) Start(ctx context.Context, args ...string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.cmd != nil { + return fmt.Errorf("coredeno: already running") + } + + // Ensure socket directory exists + sockDir := filepath.Dir(s.opts.SocketPath) + if err := os.MkdirAll(sockDir, 0755); err != nil { + return fmt.Errorf("coredeno: mkdir %s: %w", sockDir, err) + } + + // Remove stale socket + os.Remove(s.opts.SocketPath) + + s.ctx, s.cancel = context.WithCancel(ctx) + s.cmd = exec.CommandContext(s.ctx, s.opts.DenoPath, args...) + s.done = make(chan struct{}) + if err := s.cmd.Start(); err != nil { + s.cmd = nil + s.cancel() + return fmt.Errorf("coredeno: start: %w", err) + } + + // Monitor in background — waits for exit, then signals done + go func() { + s.cmd.Wait() + s.mu.Lock() + s.cmd = nil + s.mu.Unlock() + close(s.done) + }() + return nil +} + +// Stop cancels the context and waits for the process to exit. +func (s *Sidecar) Stop() error { + s.mu.RLock() + if s.cmd == nil { + s.mu.RUnlock() + return nil + } + done := s.done + s.mu.RUnlock() + + s.cancel() + <-done + return nil +} + +// IsRunning returns true if the sidecar process is alive. +func (s *Sidecar) IsRunning() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.cmd != nil +} diff --git a/pkg/coredeno/lifecycle_test.go b/pkg/coredeno/lifecycle_test.go new file mode 100644 index 0000000..a8ff90f --- /dev/null +++ b/pkg/coredeno/lifecycle_test.go @@ -0,0 +1,56 @@ +package coredeno + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStart_Good(t *testing.T) { + sockDir := t.TempDir() + sc := NewSidecar(Options{ + DenoPath: "sleep", + SocketPath: filepath.Join(sockDir, "test.sock"), + }) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + err := sc.Start(ctx, "10") // sleep 10 — will be killed by Stop + require.NoError(t, err) + assert.True(t, sc.IsRunning()) + + err = sc.Stop() + require.NoError(t, err) + assert.False(t, sc.IsRunning()) +} + +func TestStop_Good_NotStarted(t *testing.T) { + sc := NewSidecar(Options{DenoPath: "sleep"}) + err := sc.Stop() + assert.NoError(t, err, "stopping a not-started sidecar should be a no-op") +} + +func TestSocketDirCreated_Good(t *testing.T) { + dir := t.TempDir() + sockPath := filepath.Join(dir, "sub", "deno.sock") + sc := NewSidecar(Options{ + DenoPath: "sleep", + SocketPath: sockPath, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + err := sc.Start(ctx, "10") + require.NoError(t, err) + defer sc.Stop() + + _, err = os.Stat(filepath.Join(dir, "sub")) + assert.NoError(t, err, "socket directory should be created") +}