Reticulum-Go/pkg/sharedinstance/instance.go

186 lines
5.1 KiB
Go
Raw Permalink Normal View History

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
//go:build !tinygo
package sharedinstance
import (
"fmt"
"strings"
"quad4/reticulum-go/pkg/backbone"
"quad4/reticulum-go/pkg/common"
"quad4/reticulum-go/pkg/debug"
"quad4/reticulum-go/pkg/interfaces"
"quad4/reticulum-go/pkg/transport"
)
// Mode describes how this process participates in the shared instance.
type Mode int
const (
ModeDisabled Mode = iota
ModeServer
ModeClient
)
// Instance holds shared-instance state for a running node.
type Instance struct {
Mode Mode
Server *interfaces.LocalServerInterface
Client *interfaces.LocalClientInterface
RPC *RPCServer
}
// Hooks wires shared-instance clients into the transport stack.
type Hooks struct {
RegisterInterface func(name string, iface common.NetworkInterface) error
UnregisterInterface func(name string)
HandleInterface func(iface common.NetworkInterface)
OnClientAttach func()
}
// Attach starts or joins a shared local instance when share_instance is
// enabled.
//
// With an explicit shared_instance_type it binds as server first, then falls
// back to client (Python behavior).
//
// When the type is unset it matches Python platform defaults (Unix on Linux,
// TCP elsewhere). Before binding, it tries client dials on the primary then
// alternate transport so utilities can join stock Python Unix or a TCP Go
// daemon without rewriting config.
func Attach(cfg *common.ReticulumConfig, tr *transport.Transport, hooks Hooks) (*Instance, error) {
if cfg == nil || !cfg.ShareInstance {
return &Instance{Mode: ModeDisabled}, nil
}
auto := strings.TrimSpace(cfg.SharedInstanceType) == ""
typ := common.ResolveSharedInstanceType(cfg.SharedInstanceType)
useUnix := typ == common.SharedInstanceUnix
socketPath := cfg.InstanceName
if socketPath == "" && useUnix {
socketPath = "default"
}
if auto {
order := []bool{useUnix}
if useUnix {
order = append(order, false)
} else {
order = append(order, true)
}
for _, unix := range order {
path := socketPath
if unix && path == "" {
path = "default"
}
if inst, err := attachClient(cfg, tr, hooks, cfg.SharedInstancePort, path, unix); err == nil {
return inst, nil
}
}
}
return attachServerOrClient(cfg, tr, hooks, cfg.SharedInstancePort, socketPath, useUnix)
}
func attachServerOrClient(cfg *common.ReticulumConfig, tr *transport.Transport, hooks Hooks, port int, socketPath string, useUnix bool) (*Instance, error) {
inst := &Instance{}
spawn := func(client *interfaces.LocalClientInterface) {
if hooks.RegisterInterface == nil || hooks.HandleInterface == nil {
return
}
if err := hooks.RegisterInterface(client.GetName(), client); err != nil {
debug.Log(debug.DebugCritical, "Failed to register spawned local client", "error", err)
return
}
hooks.HandleInterface(client)
}
server, err := interfaces.NewLocalServerInterface(port, socketPath, useUnix, spawn, backbone.Get())
if err != nil {
return nil, err
}
if err := server.Start(); err == nil {
inst.Mode = ModeServer
inst.Server = server
if hooks.RegisterInterface != nil {
if err := hooks.RegisterInterface(server.GetName(), server); err != nil {
_ = server.Stop()
return nil, fmt.Errorf("register shared server: %w", err)
}
}
// Persist resolved transport so RPC listen matches the data plane.
if strings.TrimSpace(cfg.SharedInstanceType) == "" {
if useUnix {
cfg.SharedInstanceType = common.SharedInstanceUnix
} else {
cfg.SharedInstanceType = common.SharedInstanceTCP
}
}
rpc, err := StartRPCServer(cfg, tr)
if err != nil {
_ = server.Stop()
return nil, err
}
inst.RPC = rpc
debug.Log(debug.DebugInfo, "Started shared instance server", "port", port, "unix", useUnix)
return inst, nil
}
return attachClient(cfg, tr, hooks, port, socketPath, useUnix)
}
func attachClient(cfg *common.ReticulumConfig, tr *transport.Transport, hooks Hooks, port int, socketPath string, useUnix bool) (*Instance, error) {
client, err := interfaces.NewLocalClientInterface(port, socketPath, useUnix, backbone.Get())
if err != nil {
return nil, err
}
client.SetDisconnectHooks(
2026-07-18 08:30:30 -05:00
func() { tr.SetConnectedToSharedInstance(false) },
func() { tr.SetConnectedToSharedInstance(true) },
)
if err := client.Start(); err != nil {
return nil, fmt.Errorf("connect to local shared instance: %w", err)
}
tr.SetConnectedToSharedInstance(true)
cfg.ConnectedToSharedInstance = true
inst := &Instance{Mode: ModeClient, Client: client}
if hooks.RegisterInterface != nil {
if err := hooks.RegisterInterface(client.GetName(), client); err != nil {
_ = client.Stop()
return nil, err
}
}
if hooks.HandleInterface != nil {
hooks.HandleInterface(client)
}
if hooks.OnClientAttach != nil {
hooks.OnClientAttach()
}
debug.Log(debug.DebugInfo, "Connected to existing local shared instance", "unix", useUnix)
return inst, nil
}
func (i *Instance) Close() {
if i == nil {
return
}
if i.RPC != nil {
_ = i.RPC.Close()
}
if i.Server != nil {
_ = i.Server.Stop()
}
if i.Client != nil {
_ = i.Client.Stop()
}
}
func (i *Instance) OwnsNetworkInterfaces() bool {
if i == nil {
return true
}
return i.Mode != ModeClient
}