Wire the CoreDeno sidecar into a fully bidirectional bridge: - Deno→Go (gRPC): Deno connects as CoreService client via polyfilled @grpc/grpc-js over Unix socket. Polyfill patches Deno 2.x http2 gaps (getDefaultSettings, pre-connected socket handling, remoteSettings). - Go→Deno (JSON-RPC): Go connects to Deno's newline-delimited JSON-RPC server for module lifecycle (LoadModule, UnloadModule, ModuleStatus). gRPC server direction avoided due to Deno http2.createServer limitations. - ProcessStart/ProcessStop: gRPC handlers delegate to process.Service with manifest permission gating (run permissions). - Deno runtime: main.ts boots DenoService server, connects CoreService client with retry + health-check round-trip, handles SIGTERM shutdown. 40 unit tests + 2 integration tests (Tier 1 boot + Tier 2 bidirectional). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
75 lines
1.6 KiB
Go
75 lines
1.6 KiB
Go
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 Deno socket (the Core socket is managed by ListenGRPC)
|
|
if s.opts.DenoSocketPath != "" {
|
|
os.Remove(s.opts.DenoSocketPath)
|
|
}
|
|
|
|
s.ctx, s.cancel = context.WithCancel(ctx)
|
|
s.cmd = exec.CommandContext(s.ctx, s.opts.DenoPath, args...)
|
|
s.cmd.Env = append(os.Environ(),
|
|
"CORE_SOCKET="+s.opts.SocketPath,
|
|
"DENO_SOCKET="+s.opts.DenoSocketPath,
|
|
)
|
|
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
|
|
}
|