feat(coredeno): sidecar Start/Stop/IsRunning lifecycle

Process launch with context cancellation, socket directory auto-creation,
channel-based stop synchronization. Uses sleep as fake Deno in tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-02-17 20:52:58 +00:00
parent 6e283c1284
commit e341fbcdcf
No known key found for this signature in database
GPG key ID: AF404715446AEB41
3 changed files with 126 additions and 0 deletions

View file

@ -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.

69
pkg/coredeno/lifecycle.go Normal file
View file

@ -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
}

View file

@ -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")
}