- Added ContainerWorkDir and ContainerIPCSocket fields to the Config struct for better container path configuration. - Updated DefaultConfig to initialize new fields with default values. - Modified the Manager to apply defaults for container paths and utilize them in container creation and execution methods, improving flexibility and reliability in container management.
699 lines
17 KiB
Go
699 lines
17 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/client"
|
|
"github.com/yaoapp/yao/sandbox/ipc"
|
|
)
|
|
|
|
// execReadCloser wraps a Reader with a Closer
|
|
type execReadCloser struct {
|
|
*bufio.Reader
|
|
closer io.Closer
|
|
}
|
|
|
|
func (e *execReadCloser) Close() error {
|
|
if e.closer != nil {
|
|
return e.closer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Manager manages sandbox containers
|
|
type Manager struct {
|
|
mu sync.Mutex // Protects creation
|
|
containers sync.Map // containerName → *Container
|
|
running int32 // Running container count
|
|
ipcManager *ipc.Manager // IPC manager
|
|
dockerClient *client.Client // Docker client
|
|
config *Config // Configuration
|
|
}
|
|
|
|
// NewManager creates a new sandbox manager
|
|
func NewManager(config *Config) (*Manager, error) {
|
|
if config == nil {
|
|
config = DefaultConfig()
|
|
}
|
|
|
|
// Apply defaults for missing container paths
|
|
if config.ContainerWorkDir == "" {
|
|
config.ContainerWorkDir = "/workspace"
|
|
}
|
|
if config.ContainerIPCSocket == "" {
|
|
config.ContainerIPCSocket = "/tmp/yao.sock"
|
|
}
|
|
|
|
// Initialize Docker client
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Docker client: %w", err)
|
|
}
|
|
|
|
// Ping Docker to verify connection
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if _, err := cli.Ping(ctx); err != nil {
|
|
cli.Close()
|
|
return nil, fmt.Errorf("%w: %v", ErrDockerNotAvailable, err)
|
|
}
|
|
|
|
// Ensure directories exist
|
|
if err := os.MkdirAll(config.WorkspaceRoot, 0755); err != nil {
|
|
cli.Close()
|
|
return nil, fmt.Errorf("failed to create workspace directory: %w", err)
|
|
}
|
|
if err := os.MkdirAll(config.IPCDir, 0755); err != nil {
|
|
cli.Close()
|
|
return nil, fmt.Errorf("failed to create IPC directory: %w", err)
|
|
}
|
|
|
|
m := &Manager{
|
|
dockerClient: cli,
|
|
config: config,
|
|
ipcManager: ipc.NewManager(config.IPCDir),
|
|
}
|
|
|
|
// Start cleanup loop
|
|
go m.startCleanupLoop(context.Background())
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Close closes the manager and cleans up resources
|
|
func (m *Manager) Close() error {
|
|
m.ipcManager.CloseAll()
|
|
return m.dockerClient.Close()
|
|
}
|
|
|
|
// GetOrCreate returns existing container or creates new one
|
|
func (m *Manager) GetOrCreate(ctx context.Context, userID, chatID string) (*Container, error) {
|
|
name := containerName(userID, chatID)
|
|
|
|
// Check if container already exists (fast path)
|
|
if c, ok := m.containers.Load(name); ok {
|
|
cont := c.(*Container)
|
|
cont.LastUsedAt = time.Now()
|
|
return cont, nil
|
|
}
|
|
|
|
// Use mutex for creation to avoid race condition
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Double-check after acquiring lock
|
|
if c, ok := m.containers.Load(name); ok {
|
|
cont := c.(*Container)
|
|
cont.LastUsedAt = time.Now()
|
|
return cont, nil
|
|
}
|
|
|
|
// Check running container limit
|
|
if m.running >= int32(m.config.MaxContainers) {
|
|
return nil, ErrTooManyContainers
|
|
}
|
|
|
|
// Create new container
|
|
cont, err := m.createContainer(ctx, userID, chatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store and increment counter
|
|
m.containers.Store(name, cont)
|
|
m.running++
|
|
|
|
return cont, nil
|
|
}
|
|
|
|
// createContainer creates a new Docker container
|
|
func (m *Manager) createContainer(ctx context.Context, userID, chatID string) (*Container, error) {
|
|
name := containerName(userID, chatID)
|
|
|
|
// Ensure image exists, pull if not
|
|
if err := m.ensureImage(ctx, m.config.Image); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Workspace directory
|
|
workspaceHost := filepath.Join(m.config.WorkspaceRoot, userID, chatID)
|
|
if err := os.MkdirAll(workspaceHost, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create workspace: %w", err)
|
|
}
|
|
|
|
// IPC socket path
|
|
sessionID := chatID
|
|
ipcSocketHost := filepath.Join(m.config.IPCDir, sessionID+".sock")
|
|
|
|
// Container configuration
|
|
containerConfig := &container.Config{
|
|
Image: m.config.Image,
|
|
Cmd: []string{"sleep", "infinity"},
|
|
WorkingDir: m.config.ContainerWorkDir,
|
|
Env: []string{
|
|
"YAO_IPC_SOCKET=" + m.config.ContainerIPCSocket,
|
|
},
|
|
}
|
|
|
|
// Host configuration - only mount IPC socket if it exists
|
|
binds := []string{
|
|
workspaceHost + ":" + m.config.ContainerWorkDir,
|
|
}
|
|
// Only mount IPC socket if the file exists (it's created by IPC manager)
|
|
if _, err := os.Stat(ipcSocketHost); err == nil {
|
|
binds = append(binds, ipcSocketHost+":"+m.config.ContainerIPCSocket)
|
|
}
|
|
|
|
hostConfig := &container.HostConfig{
|
|
Binds: binds,
|
|
Resources: container.Resources{
|
|
Memory: parseMemory(m.config.MaxMemory),
|
|
NanoCPUs: int64(m.config.MaxCPU * 1e9),
|
|
},
|
|
SecurityOpt: []string{"no-new-privileges"},
|
|
CapDrop: []string{"ALL"},
|
|
}
|
|
|
|
// Create container
|
|
resp, err := m.dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create container: %w", err)
|
|
}
|
|
|
|
return &Container{
|
|
ID: resp.ID,
|
|
Name: name,
|
|
UserID: userID,
|
|
ChatID: chatID,
|
|
Status: StatusCreated,
|
|
CreatedAt: time.Now(),
|
|
LastUsedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// ensureImage ensures the image exists locally, pulls if not
|
|
func (m *Manager) ensureImage(ctx context.Context, imageName string) error {
|
|
// Check if image exists locally
|
|
_, _, err := m.dockerClient.ImageInspectWithRaw(ctx, imageName)
|
|
if err == nil {
|
|
return nil // Image exists
|
|
}
|
|
|
|
// Image not found, pull it
|
|
reader, err := m.dockerClient.ImagePull(ctx, imageName, image.PullOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pull image %s: %w", imageName, err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Wait for pull to complete by reading the response
|
|
_, err = io.Copy(io.Discard, reader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pull image %s: %w", imageName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ensureRunning ensures the container is running
|
|
func (m *Manager) ensureRunning(ctx context.Context, name string) error {
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
if cont.Status == StatusRunning {
|
|
return nil
|
|
}
|
|
|
|
// Start the container
|
|
if err := m.dockerClient.ContainerStart(ctx, cont.ID, container.StartOptions{}); err != nil {
|
|
return fmt.Errorf("failed to start container: %w", err)
|
|
}
|
|
|
|
// Wait for container to be ready (inspect until running)
|
|
for i := 0; i < 30; i++ {
|
|
info, err := m.dockerClient.ContainerInspect(ctx, cont.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to inspect container: %w", err)
|
|
}
|
|
if info.State.Running {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
m.mu.Lock()
|
|
cont.Status = StatusRunning
|
|
cont.LastUsedAt = time.Now()
|
|
m.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stream executes command and returns stdout reader
|
|
func (m *Manager) Stream(ctx context.Context, name string, cmd []string, opts *ExecOptions) (io.ReadCloser, error) {
|
|
// Ensure container is running
|
|
if err := m.ensureRunning(ctx, name); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get container
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return nil, ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
// Update last used time
|
|
cont.LastUsedAt = time.Now()
|
|
|
|
// Default options
|
|
if opts == nil {
|
|
opts = &ExecOptions{}
|
|
}
|
|
if opts.WorkDir == "" {
|
|
opts.WorkDir = "/workspace"
|
|
}
|
|
|
|
// Create exec instance
|
|
execConfig := container.ExecOptions{
|
|
Cmd: cmd,
|
|
WorkingDir: opts.WorkDir,
|
|
Env: mapToSlice(opts.Env),
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
AttachStdin: opts.Stdin != nil,
|
|
}
|
|
|
|
execResp, err := m.dockerClient.ContainerExecCreate(ctx, cont.ID, execConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create exec: %w", err)
|
|
}
|
|
|
|
// Attach to exec
|
|
attachResp, err := m.dockerClient.ContainerExecAttach(ctx, execResp.ID, container.ExecStartOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to attach to exec: %w", err)
|
|
}
|
|
|
|
// Handle stdin if provided
|
|
if opts.Stdin != nil {
|
|
go func() {
|
|
io.Copy(attachResp.Conn, opts.Stdin)
|
|
attachResp.CloseWrite()
|
|
}()
|
|
}
|
|
|
|
// Wrap in a ReadCloser
|
|
return &execReadCloser{
|
|
Reader: attachResp.Reader,
|
|
closer: attachResp.Conn,
|
|
}, nil
|
|
}
|
|
|
|
// Exec executes command and waits for completion
|
|
func (m *Manager) Exec(ctx context.Context, name string, cmd []string, opts *ExecOptions) (*ExecResult, error) {
|
|
if opts == nil {
|
|
opts = &ExecOptions{}
|
|
}
|
|
|
|
// Apply timeout if specified
|
|
if opts.Timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
// Ensure container is running
|
|
if err := m.ensureRunning(ctx, name); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get container
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return nil, ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
// Update last used time
|
|
cont.LastUsedAt = time.Now()
|
|
|
|
// Default options
|
|
if opts.WorkDir == "" {
|
|
opts.WorkDir = m.config.ContainerWorkDir
|
|
}
|
|
|
|
// Create exec instance
|
|
execConfig := container.ExecOptions{
|
|
Cmd: cmd,
|
|
WorkingDir: opts.WorkDir,
|
|
Env: mapToSlice(opts.Env),
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
AttachStdin: opts.Stdin != nil,
|
|
}
|
|
|
|
execResp, err := m.dockerClient.ContainerExecCreate(ctx, cont.ID, execConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create exec: %w", err)
|
|
}
|
|
|
|
// Attach to exec
|
|
attachResp, err := m.dockerClient.ContainerExecAttach(ctx, execResp.ID, container.ExecStartOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to attach to exec: %w", err)
|
|
}
|
|
defer attachResp.Close()
|
|
|
|
// Handle stdin if provided
|
|
if opts.Stdin != nil {
|
|
go func() {
|
|
io.Copy(attachResp.Conn, opts.Stdin)
|
|
attachResp.CloseWrite()
|
|
}()
|
|
}
|
|
|
|
// Read output with context awareness
|
|
outputCh := make(chan []byte, 1)
|
|
errCh := make(chan error, 1)
|
|
|
|
go func() {
|
|
output, err := io.ReadAll(attachResp.Reader)
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
outputCh <- output
|
|
}()
|
|
|
|
var output []byte
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case err := <-errCh:
|
|
return nil, fmt.Errorf("failed to read output: %w", err)
|
|
case output = <-outputCh:
|
|
// Output received
|
|
}
|
|
|
|
// Wait for exec to complete and get exit code
|
|
var exitCode int
|
|
for i := 0; i < 100; i++ { // Max 10 seconds wait
|
|
inspect, err := m.dockerClient.ContainerExecInspect(ctx, execResp.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to inspect exec: %w", err)
|
|
}
|
|
if !inspect.Running {
|
|
exitCode = inspect.ExitCode
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// Parse Docker multiplexed stream
|
|
// TODO: Properly demux stdout/stderr from Docker stream
|
|
stdout := string(output)
|
|
|
|
return &ExecResult{
|
|
ExitCode: exitCode,
|
|
Stdout: stdout,
|
|
Stderr: "",
|
|
}, nil
|
|
}
|
|
|
|
// Start starts a stopped container
|
|
func (m *Manager) Start(ctx context.Context, name string) error {
|
|
return m.ensureRunning(ctx, name)
|
|
}
|
|
|
|
// Stop stops container but preserves data
|
|
func (m *Manager) Stop(ctx context.Context, name string) error {
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
// Only stop if running
|
|
if cont.Status != StatusRunning {
|
|
return nil
|
|
}
|
|
|
|
if err := m.dockerClient.ContainerStop(ctx, cont.ID, container.StopOptions{}); err != nil {
|
|
// Ignore "not running" error
|
|
if !strings.Contains(err.Error(), "is not running") {
|
|
return fmt.Errorf("failed to stop container: %w", err)
|
|
}
|
|
}
|
|
|
|
// Update status, decrement running count
|
|
m.mu.Lock()
|
|
if cont.Status == StatusRunning {
|
|
cont.Status = StatusStopped
|
|
m.running--
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove deletes container and its data
|
|
func (m *Manager) Remove(ctx context.Context, name string) error {
|
|
// Stop first if running
|
|
m.Stop(ctx, name)
|
|
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
// Close IPC session
|
|
m.ipcManager.Close(cont.ChatID)
|
|
|
|
if err := m.dockerClient.ContainerRemove(ctx, cont.ID, container.RemoveOptions{Force: true}); err != nil {
|
|
return fmt.Errorf("failed to remove container: %w", err)
|
|
}
|
|
|
|
// Remove from map
|
|
m.containers.Delete(name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// List returns all containers for a user
|
|
func (m *Manager) List(ctx context.Context, userID string) ([]*Container, error) {
|
|
var result []*Container
|
|
prefix := fmt.Sprintf("yao-sandbox-%s-", userID)
|
|
|
|
m.containers.Range(func(key, value interface{}) bool {
|
|
name := key.(string)
|
|
if strings.HasPrefix(name, prefix) {
|
|
result = append(result, value.(*Container))
|
|
}
|
|
return true
|
|
})
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Cleanup stops idle containers
|
|
func (m *Manager) Cleanup(ctx context.Context) error {
|
|
now := time.Now()
|
|
|
|
m.containers.Range(func(key, value interface{}) bool {
|
|
name := key.(string)
|
|
c := value.(*Container)
|
|
|
|
// Stop idle containers
|
|
if c.Status == StatusRunning && now.Sub(c.LastUsedAt) > m.config.IdleTimeout {
|
|
m.Stop(ctx, name)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// startCleanupLoop starts the periodic cleanup loop
|
|
func (m *Manager) startCleanupLoop(ctx context.Context) {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
m.Cleanup(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// WriteFile writes content to a file in container
|
|
func (m *Manager) WriteFile(ctx context.Context, name, path string, content []byte) error {
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
// Ensure parent directory exists
|
|
dir := filepath.Dir(path)
|
|
if dir != "/" && dir != "." {
|
|
result, err := m.Exec(ctx, name, []string{"mkdir", "-p", dir}, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create parent directory: %w", err)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
return fmt.Errorf("mkdir failed with exit code %d: %s", result.ExitCode, result.Stdout)
|
|
}
|
|
|
|
// Verify directory was created
|
|
verifyResult, err := m.Exec(ctx, name, []string{"test", "-d", dir}, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify directory: %w", err)
|
|
}
|
|
if verifyResult.ExitCode != 0 {
|
|
return fmt.Errorf("directory %s was not created", dir)
|
|
}
|
|
}
|
|
|
|
// Create a tar archive with the file
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
|
|
hdr := &tar.Header{
|
|
Name: filepath.Base(path),
|
|
Mode: 0644,
|
|
Size: int64(len(content)),
|
|
}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tw.Write(content); err != nil {
|
|
return err
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy to container
|
|
return m.dockerClient.CopyToContainer(ctx, cont.ID, dir, &buf, container.CopyToContainerOptions{})
|
|
}
|
|
|
|
// ReadFile reads content from a file in container
|
|
func (m *Manager) ReadFile(ctx context.Context, name, path string) ([]byte, error) {
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return nil, ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
reader, _, err := m.dockerClient.CopyFromContainer(ctx, cont.ID, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Extract from tar
|
|
tr := tar.NewReader(reader)
|
|
_, err = tr.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return io.ReadAll(tr)
|
|
}
|
|
|
|
// ListDir lists directory contents in container
|
|
func (m *Manager) ListDir(ctx context.Context, name, path string) ([]FileInfo, error) {
|
|
result, err := m.Exec(ctx, name, []string{"ls", "-la", "--time-style=+%s", path}, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return parseLS(result.Stdout), nil
|
|
}
|
|
|
|
// Stat returns file info
|
|
func (m *Manager) Stat(ctx context.Context, name, path string) (*FileInfo, error) {
|
|
result, err := m.Exec(ctx, name, []string{"stat", "--format=%n|%s|%f|%Y|%F", path}, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseStat(result.Stdout), nil
|
|
}
|
|
|
|
// MkDir creates directory in container
|
|
func (m *Manager) MkDir(ctx context.Context, name, path string) error {
|
|
_, err := m.Exec(ctx, name, []string{"mkdir", "-p", path}, nil)
|
|
return err
|
|
}
|
|
|
|
// RemoveFile removes file or directory in container
|
|
func (m *Manager) RemoveFile(ctx context.Context, name, path string) error {
|
|
_, err := m.Exec(ctx, name, []string{"rm", "-rf", path}, nil)
|
|
return err
|
|
}
|
|
|
|
// CopyToContainer copies from host to container
|
|
func (m *Manager) CopyToContainer(ctx context.Context, name, hostPath, containerPath string) error {
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
// Create tar archive from host path
|
|
archive, err := createTarFromPath(hostPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer archive.Close()
|
|
|
|
return m.dockerClient.CopyToContainer(ctx, cont.ID, containerPath, archive, container.CopyToContainerOptions{})
|
|
}
|
|
|
|
// CopyFromContainer copies from container to host
|
|
func (m *Manager) CopyFromContainer(ctx context.Context, name, containerPath, hostPath string) error {
|
|
c, ok := m.containers.Load(name)
|
|
if !ok {
|
|
return ErrContainerNotFound
|
|
}
|
|
cont := c.(*Container)
|
|
|
|
reader, _, err := m.dockerClient.CopyFromContainer(ctx, cont.ID, containerPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
|
|
return extractTarToPath(reader, hostPath)
|
|
}
|
|
|
|
// GetIPCManager returns the IPC manager
|
|
func (m *Manager) GetIPCManager() *ipc.Manager {
|
|
return m.ipcManager
|
|
}
|
|
|
|
// GetConfig returns the configuration
|
|
func (m *Manager) GetConfig() *Config {
|
|
return m.config
|
|
}
|