anonpenguin23 cbbf72092d feat: add Rqlite MCP server and presence functionality
- Introduced a new Rqlite MCP server implementation in `cmd/rqlite-mcp`, enabling JSON-RPC communication for database operations.
- Updated the Makefile to include the build command for the Rqlite MCP server.
- Enhanced the WebSocket PubSub client with presence capabilities, allowing members to join and leave topics with notifications.
- Implemented presence management in the gateway, including endpoints for querying current members in a topic.
- Added end-to-end tests for presence functionality, ensuring correct behavior during member join and leave events.
2026-01-03 14:25:13 +02:00

218 lines
5.8 KiB
Go

package development
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"sync"
"time"
)
// ProcessManager manages all dev environment processes
type ProcessManager struct {
oramaDir string
pidsDir string
processes map[string]*ManagedProcess
mutex sync.Mutex
logWriter io.Writer
}
// ManagedProcess tracks a running process
type ManagedProcess struct {
Name string
PID int
StartTime time.Time
LogPath string
}
// NewProcessManager creates a new process manager
func NewProcessManager(oramaDir string, logWriter io.Writer) *ProcessManager {
pidsDir := filepath.Join(oramaDir, ".pids")
os.MkdirAll(pidsDir, 0755)
return &ProcessManager{
oramaDir: oramaDir,
pidsDir: pidsDir,
processes: make(map[string]*ManagedProcess),
logWriter: logWriter,
}
}
// StartAll starts all development services
func (pm *ProcessManager) StartAll(ctx context.Context) error {
fmt.Fprintf(pm.logWriter, "\n🚀 Starting development environment...\n")
fmt.Fprintf(pm.logWriter, "═══════════════════════════════════════\n\n")
topology := DefaultTopology()
// Build IPFS node info from topology
ipfsNodes := pm.buildIPFSNodes(topology)
// Start in order of dependencies
services := []struct {
name string
fn func(context.Context) error
}{
{"IPFS", pm.startIPFS},
{"IPFS Cluster", pm.startIPFSCluster},
{"Olric", pm.startOlric},
{"Anon", pm.startAnon},
{"Nodes (Network)", pm.startNodes},
{"Rqlite MCP", pm.startMCP},
}
for _, svc := range services {
if err := svc.fn(ctx); err != nil {
fmt.Fprintf(pm.logWriter, "⚠️ Failed to start %s: %v\n", svc.name, err)
}
}
fmt.Fprintf(pm.logWriter, "\n")
// Run health checks with retries before declaring success
const (
healthCheckRetries = 20
healthCheckInterval = 3 * time.Second
healthCheckTimeout = 70 * time.Second
)
if !pm.HealthCheckWithRetry(ctx, ipfsNodes, healthCheckRetries, healthCheckInterval, healthCheckTimeout) {
fmt.Fprintf(pm.logWriter, "\n❌ Health checks failed - stopping all services\n")
pm.StopAll(ctx)
return fmt.Errorf("cluster health checks failed - services stopped")
}
// Print success and key endpoints
pm.printStartupSummary(topology)
return nil
}
// StopAll stops all running processes
func (pm *ProcessManager) StopAll(ctx context.Context) error {
fmt.Fprintf(pm.logWriter, "\n🛑 Stopping development environment...\n\n")
topology := DefaultTopology()
var services []string
// Build service list from topology (in reverse order)
services = append(services, "gateway")
for i := len(topology.Nodes) - 1; i >= 0; i-- {
node := topology.Nodes[i]
services = append(services, node.Name)
}
for i := len(topology.Nodes) - 1; i >= 0; i-- {
node := topology.Nodes[i]
services = append(services, fmt.Sprintf("ipfs-cluster-%s", node.Name))
}
for i := len(topology.Nodes) - 1; i >= 0; i-- {
node := topology.Nodes[i]
services = append(services, fmt.Sprintf("ipfs-%s", node.Name))
}
services = append(services, "olric", "anon", "rqlite-mcp")
fmt.Fprintf(pm.logWriter, "Stopping %d services...\n\n", len(services))
stoppedCount := 0
for _, svc := range services {
if err := pm.stopProcess(svc); err != nil {
fmt.Fprintf(pm.logWriter, "⚠️ Error stopping %s: %v\n", svc, err)
} else {
stoppedCount++
}
fmt.Fprintf(pm.logWriter, " [%d/%d] stopped\n", stoppedCount, len(services))
}
fmt.Fprintf(pm.logWriter, "\n✅ All %d services have been stopped\n\n", stoppedCount)
return nil
}
// Status reports the status of all services
func (pm *ProcessManager) Status(ctx context.Context) {
fmt.Fprintf(pm.logWriter, "\n📊 Development Environment Status\n")
fmt.Fprintf(pm.logWriter, "================================\n\n")
topology := DefaultTopology()
// Build service list from topology
var services []struct {
name string
ports []int
}
for _, node := range topology.Nodes {
services = append(services, struct {
name string
ports []int
}{
fmt.Sprintf("%s IPFS", node.Name),
[]int{node.IPFSAPIPort, node.IPFSSwarmPort},
})
services = append(services, struct {
name string
ports []int
}{
fmt.Sprintf("%s Cluster", node.Name),
[]int{node.ClusterAPIPort},
})
services = append(services, struct {
name string
ports []int
}{
fmt.Sprintf("%s Node (P2P)", node.Name),
[]int{node.P2PPort},
})
}
services = append(services, struct {
name string
ports []int
}{"Gateway", []int{topology.GatewayPort}})
services = append(services, struct {
name string
ports []int
}{"Olric", []int{topology.OlricHTTPPort, topology.OlricMemberPort}})
services = append(services, struct {
name string
ports []int
}{"Anon SOCKS", []int{topology.AnonSOCKSPort}})
services = append(services, struct {
name string
ports []int
}{"Rqlite MCP", []int{topology.MCPPort}})
for _, svc := range services {
pidPath := filepath.Join(pm.pidsDir, fmt.Sprintf("%s.pid", svc.name))
running := false
if pidBytes, err := os.ReadFile(pidPath); err == nil {
var pid int
fmt.Sscanf(string(pidBytes), "%d", &pid)
if checkProcessRunning(pid) {
running = true
}
}
status := "❌ stopped"
if running {
status = "✅ running"
}
portStr := fmt.Sprintf("ports: %v", svc.ports)
fmt.Fprintf(pm.logWriter, " %-25s %s (%s)\n", svc.name, status, portStr)
}
fmt.Fprintf(pm.logWriter, "\nConfiguration files in %s:\n", pm.oramaDir)
configFiles := []string{"node-1.yaml", "node-2.yaml", "node-3.yaml", "node-4.yaml", "node-5.yaml", "olric-config.yaml"}
for _, f := range configFiles {
path := filepath.Join(pm.oramaDir, f)
if _, err := os.Stat(path); err == nil {
fmt.Fprintf(pm.logWriter, " ✓ %s\n", f)
} else {
fmt.Fprintf(pm.logWriter, " ✗ %s\n", f)
}
}
fmt.Fprintf(pm.logWriter, "\nLogs directory: %s/logs\n\n", pm.oramaDir)
}