diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 85c58d7..1a4d69d 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -3,19 +3,14 @@ package main import ( - "context" "flag" "log" "os" - "path/filepath" - "github.com/waste-go/internal/anchor" "github.com/waste-go/internal/crypto" - "github.com/waste-go/internal/invite" "github.com/waste-go/internal/ipc" - "github.com/waste-go/internal/mesh" - "github.com/waste-go/internal/proto" - "github.com/waste-go/internal/store" + "github.com/waste-go/internal/invite" + "github.com/waste-go/internal/netmgr" ) func main() { @@ -27,6 +22,14 @@ func main() { joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup") flag.Parse() + dir := expandHome(*dataDir) + + id, err := crypto.LoadOrCreate(dir, *alias) + if err != nil { + log.Fatalf("identity: %v", err) + } + log.Printf("daemon: local peer id: %s alias: %s", id.PeerID().Short(), id.Alias) + // --join overrides/sets the anchor URL and triggers an auto-join. var autoJoinNetwork string if *joinInvite != "" { @@ -39,45 +42,20 @@ func main() { log.Printf("daemon: invite decoded — anchor=%s network=%s", inv.Anchor, inv.Network) } - dir := expandHome(*dataDir) - id, err := crypto.LoadOrCreate(dir, *alias) - if err != nil { - log.Fatalf("identity: %v", err) - } - log.Printf("daemon: local peer id: %s alias: %s", id.PeerID().Short(), id.Alias) + mgr := netmgr.New(netmgr.Config{ + MasterIdentity: id, + StoreDir: dir, + AnchorURL: *anchorURL, + ShareDir: expandHome(*shareDir), + }) - st, err := store.Open(filepath.Join(dir, "messages.db")) - if err != nil { - log.Fatalf("store: %v", err) - } - defer st.Close() - - m := mesh.New(id, st) - if *shareDir != "" { - m.ShareDir = expandHome(*shareDir) - log.Printf("daemon: sharing %s", m.ShareDir) - } - - // joinFn is passed to the IPC layer; it's called when the UI sends join_network. - joinFn := func(ctx context.Context, networkName string) { - if *anchorURL == "" { - log.Printf("daemon: join_network: no -anchor flag set") - m.Emit(proto.IpcMessage{Type: proto.EvtError, ErrorMessage: "no anchor configured — start daemon with -anchor "}) - return - } - log.Printf("daemon: joining network %q via %s", networkName, *anchorURL) - anchor.Run(ctx, *anchorURL, networkName, id, m) - log.Printf("daemon: left network %q", networkName) - } - - // Auto-join from --join flag before starting IPC (non-blocking). if autoJoinNetwork != "" { - ctx, cancel := context.WithCancel(context.Background()) - _ = cancel // lifecycle managed by the joinFn / ipc.Run leave - go joinFn(ctx, autoJoinNetwork) + if _, err := mgr.Join(autoJoinNetwork); err != nil { + log.Fatalf("auto-join: %v", err) + } } - if err := ipc.Run(m, *ipcPort, *anchorURL, joinFn); err != nil { + if err := ipc.Run(mgr, *ipcPort); err != nil { log.Fatalf("ipc: %v", err) } } @@ -90,4 +68,3 @@ func expandHome(path string) string { } return path } - diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index ebbcc0c..d3fbc9f 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -15,6 +15,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "time" @@ -22,6 +23,7 @@ import ( "filippo.io/edwards25519" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" "golang.org/x/crypto/nacl/box" "github.com/waste-go/internal/proto" @@ -179,6 +181,24 @@ func SignalingOpen(b64box string, senderPub, recipientPriv *[32]byte) ([]byte, e return out, nil } +// DeriveForNetwork returns a new in-memory Identity derived from the master key +// and the given network hash (hex). Same master + same network hash always +// produces the same keypair, so the peer ID is stable across restarts. +// Different networks produce different peer IDs, preventing cross-network correlation. +func DeriveForNetwork(master *Identity, networkHash string) (*Identity, error) { + r := hkdf.New(sha256.New, master.privateKey[:32], []byte(networkHash), []byte("yaw2-net-identity")) + var seed [32]byte + if _, err := io.ReadFull(r, seed[:]); err != nil { + return nil, fmt.Errorf("deriving network identity: %w", err) + } + priv := ed25519.NewKeyFromSeed(seed[:]) + return &Identity{ + privateKey: priv, + PublicKey: priv.Public().(ed25519.PublicKey), + Alias: master.Alias, + }, nil +} + // ── X25519 ECDH ─────────────────────────────────────────────────────────────── // EphemeralKey is an X25519 keypair used for a single session. diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 6f41443..da8be19 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -1,6 +1,8 @@ // Package ipc implements the local IPC server. // The UI (or any local tool) connects to 127.0.0.1:17337 and speaks // newline-delimited JSON: send IpcMessage commands, receive IpcMessage events. +// +// Backward compat: commands without network_id route to the first joined network. package ipc import ( @@ -12,22 +14,16 @@ import ( "fmt" "log" "net" - "sync" "time" "github.com/google/uuid" "github.com/waste-go/internal/invite" - "github.com/waste-go/internal/mesh" + "github.com/waste-go/internal/netmgr" "github.com/waste-go/internal/proto" ) -// JoinFunc is called when the UI issues a join_network command. -type JoinFunc func(ctx context.Context, networkName string) - // Run starts the IPC listener. Blocks until the listener fails. -// anchorURL is the configured anchor WebSocket URL (used for invite generation). -// Network join/leave state is daemon-scoped (shared across all IPC clients). -func Run(m *mesh.Mesh, port int, anchorURL string, join JoinFunc) error { +func Run(mgr *netmgr.Manager, port int) error { addr := fmt.Sprintf("127.0.0.1:%d", port) ln, err := net.Listen("tcp", addr) if err != nil { @@ -35,62 +31,26 @@ func Run(m *mesh.Mesh, port int, anchorURL string, join JoinFunc) error { } log.Printf("ipc: listening on %s", addr) - // networkCancel / networkName are shared across all clients — any client - // can join/leave, and the join persists after the commanding client disconnects. - var ( - networkMu sync.Mutex - networkCancel context.CancelFunc - networkName string - ) - - doJoin := func(name string) { - networkMu.Lock() - if networkCancel != nil { - networkCancel() - } - ctx, cancel := context.WithCancel(context.Background()) - networkCancel = cancel - networkName = name - networkMu.Unlock() - go join(ctx, name) - } - - doLeave := func() { - networkMu.Lock() - if networkCancel != nil { - networkCancel() - networkCancel = nil - networkName = "" - } - networkMu.Unlock() - } - - currentNetwork := func() string { - networkMu.Lock() - defer networkMu.Unlock() - return networkName - } - for { conn, err := ln.Accept() if err != nil { return fmt.Errorf("ipc accept: %w", err) } log.Printf("ipc: UI client connected from %s", conn.RemoteAddr()) - go handleClient(conn, m, anchorURL, currentNetwork, doJoin, doLeave) + go handleClient(conn, mgr) } } -func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork func() string, doJoin func(string), doLeave func()) { +func handleClient(conn net.Conn, mgr *netmgr.Manager) { defer conn.Close() - events := m.Subscribe() - defer m.Unsubscribe(events) + events := mgr.Subscribe() + defer mgr.Unsubscribe(events) writeCh := make(chan []byte, 128) done := make(chan struct{}) - // Writer goroutine — sole owner of the write side of the connection. + // Writer goroutine. go func() { w := bufio.NewWriter(conn) for line := range writeCh { @@ -102,9 +62,7 @@ func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork } }() - // Event pusher — forwards mesh events to the UI client. - // recover() guards against the rare race where writeCh is closed while a - // send is in flight (closed channel panics even inside select). + // Event pusher — forwards Manager events to the UI client. go func() { defer func() { recover() }() //nolint:errcheck for { @@ -140,12 +98,8 @@ func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork } } - send(proto.IpcMessage{ - Type: proto.EvtStateSnapshot, - LocalPeer: ptr(m.Identity.PeerInfo()), - ConnectedPeers: m.ConnectedPeers(), - Rooms: []string{"general"}, - }) + // Send initial state snapshot. + send(stateSnapshot(mgr)) scanner := bufio.NewScanner(conn) for scanner.Scan() { @@ -157,11 +111,39 @@ func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork switch cmd.Type { + case proto.CmdJoinNetwork: + if cmd.NetworkName == "" { + send(errMsg("join_network: network_name is required")) + continue + } + netID, err := mgr.Join(cmd.NetworkName) + if err != nil { + send(errMsg(fmt.Sprintf("join_network: %v", err))) + continue + } + // network_joined event is emitted by Manager.Join; nothing extra needed. + _ = netID + + case proto.CmdLeaveNetwork: + if cmd.NetworkID != "" { + mgr.Leave(cmd.NetworkID) + } else { + // Backward compat: leave the first joined network. + if n := mgr.Default(); n != nil { + mgr.Leave(n.ID) + } + } + case proto.CmdSendMessage: + n := mgr.Resolve(cmd.NetworkID) + if n == nil { + send(errMsg("send_message: not joined to any network")) + continue + } msg := &proto.ChatMessage{ Mid: randomHex(16), ID: uuid.NewString(), - From: m.Identity.PeerID(), + From: n.Identity.PeerID(), To: cmd.To, Room: cmd.Room, Body: cmd.Body, @@ -172,68 +154,63 @@ func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork continue } if cmd.To != nil { - // DM — send only to the named recipient. - m.SendTo(*cmd.To, payload) + n.Mesh.SendTo(*cmd.To, payload) } else { - m.Broadcast(payload) + n.Mesh.Broadcast(payload) } - m.SaveMessage(msg) - m.Emit(proto.IpcMessage{Type: proto.EvtMessageReceived, Message: msg}) - - case proto.CmdJoinNetwork: - if cmd.NetworkName == "" { - send(errMsg("join_network: network_name is required")) - continue - } - doJoin(cmd.NetworkName) - - case proto.CmdLeaveNetwork: - doLeave() - - case proto.CmdGetState: - send(proto.IpcMessage{ - Type: proto.EvtStateSnapshot, - LocalPeer: ptr(m.Identity.PeerInfo()), - ConnectedPeers: m.ConnectedPeers(), - Rooms: []string{"general"}, + n.Mesh.SaveMessage(msg) + n.Mesh.Emit(proto.IpcMessage{ + Type: proto.EvtMessageReceived, + NetworkID: n.ID, + Message: msg, }) + case proto.CmdGetState: + send(stateSnapshot(mgr)) + case proto.CmdGetFileList: - if cmd.PeerID == nil || *cmd.PeerID == m.Identity.PeerID() { - // Local list — scan immediately. + n := mgr.Resolve(cmd.NetworkID) + if n == nil { + send(errMsg("get_file_list: not joined to any network")) + continue + } + if cmd.PeerID == nil || *cmd.PeerID == n.Identity.PeerID() { send(proto.IpcMessage{ - Type: proto.EvtFileList, - PeerID: ptr(m.Identity.PeerID()), - Files: m.ScanShareDir(), + Type: proto.EvtFileList, + NetworkID: n.ID, + PeerID: ptr(n.Identity.PeerID()), + Files: n.Mesh.ScanShareDir(), }) } else { - // Remote list — send a request over the DataChannel. - // The response arrives as EvtFileList via the mesh event bus. req, err := json.Marshal(proto.PeerMessage{Type: proto.MsgFileListReq}) if err != nil { continue } - if !m.SendTo(*cmd.PeerID, req) { + if !n.Mesh.SendTo(*cmd.PeerID, req) { send(errMsg(fmt.Sprintf("get_file_list: peer %s not connected", (*cmd.PeerID).Short()))) } } case proto.CmdGenerateInvite: - net := currentNetwork() - if net == "" { + n := mgr.Resolve(cmd.NetworkID) + if n == nil { send(errMsg("generate_invite: not currently joined to a network")) continue } - if anchorURL == "" { + if mgr.AnchorURL() == "" { send(errMsg("generate_invite: daemon was started without -anchor flag")) continue } - inv, err := invite.Encode(anchorURL, net) + inv, err := invite.Encode(mgr.AnchorURL(), n.Name) if err != nil { send(errMsg(fmt.Sprintf("generate_invite: %v", err))) continue } - send(proto.IpcMessage{Type: proto.EvtInviteGenerated, InviteString: inv}) + send(proto.IpcMessage{ + Type: proto.EvtInviteGenerated, + NetworkID: n.ID, + InviteString: inv, + }) case proto.CmdSendFile: send(errMsg("file transfer not yet implemented")) @@ -248,6 +225,37 @@ func handleClient(conn net.Conn, m *mesh.Mesh, anchorURL string, currentNetwork log.Printf("ipc: UI client disconnected") } +// stateSnapshot builds a state_snapshot covering all joined networks. +// Backward compat: local_peer and connected_peers are populated from the first network. +func stateSnapshot(mgr *netmgr.Manager) proto.IpcMessage { + all := mgr.All() + + msg := proto.IpcMessage{ + Type: proto.EvtStateSnapshot, + Rooms: []string{"general"}, + } + + var netInfos []proto.NetworkInfo + for _, n := range all { + pi := n.Identity.PeerInfo() + netInfos = append(netInfos, proto.NetworkInfo{ + NetworkID: n.ID, + NetworkName: n.Name, + LocalPeer: &pi, + }) + } + msg.Networks = netInfos + + // Backward-compat fields — populated from the first network when one exists. + if len(all) > 0 { + pi := all[0].Identity.PeerInfo() + msg.LocalPeer = &pi + msg.ConnectedPeers = all[0].Mesh.ConnectedPeers() + } + + return msg +} + func errMsg(s string) proto.IpcMessage { return proto.IpcMessage{Type: proto.EvtError, ErrorMessage: s} } @@ -259,3 +267,12 @@ func randomHex(n int) string { rand.Read(b) //nolint:errcheck return hex.EncodeToString(b) } + +// autoJoin is called by the daemon when --join is provided at startup. +// It joins the network before the IPC listener starts accepting clients. +func AutoJoin(ctx context.Context, mgr *netmgr.Manager, networkName string) { + _ = ctx // Manager owns the context internally + if _, err := mgr.Join(networkName); err != nil { + log.Printf("ipc: auto-join %q failed: %v", networkName, err) + } +} diff --git a/internal/netmgr/manager.go b/internal/netmgr/manager.go new file mode 100644 index 0000000..33b0fff --- /dev/null +++ b/internal/netmgr/manager.go @@ -0,0 +1,264 @@ +// Package netmgr manages multiple concurrent network contexts. +// Each network gets its own derived identity, mesh, store, and anchor connection. +// The Manager fans out events from all networks to IPC subscribers, tagging each +// event with the network_id so clients can route them appropriately. +package netmgr + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "log" + "path/filepath" + "sync" + + "github.com/waste-go/internal/anchor" + "github.com/waste-go/internal/crypto" + "github.com/waste-go/internal/mesh" + "github.com/waste-go/internal/proto" + "github.com/waste-go/internal/store" +) + +// Config holds the Manager's startup configuration. +type Config struct { + MasterIdentity *crypto.Identity + StoreDir string // base directory for per-network SQLite files + AnchorURL string // WebSocket anchor URL used for all networks + ShareDir string // shared file directory (global for now; per-network in future) +} + +// Network is a single joined network context. +type Network struct { + ID string // first 8 hex chars of networkHash — stable, short, opaque + Name string + Hash string // full SHA-256("yaw2-net:"+name), hex + + Identity *crypto.Identity + Mesh *mesh.Mesh + Store *store.Store + + cancel context.CancelFunc +} + +// Manager owns all joined networks and fans out their events to IPC subscribers. +type Manager struct { + cfg Config + + mu sync.RWMutex + networks map[string]*Network // keyed by Network.ID + order []string // insertion order for "default" lookups + + subsMu sync.Mutex + subs []chan proto.IpcMessage +} + +// New creates a Manager from the given config. +func New(cfg Config) *Manager { + return &Manager{ + networks: make(map[string]*Network), + cfg: cfg, + } +} + +// Join creates or rejoins a named network. Returns the network ID. +// If the network is already joined the existing ID is returned immediately. +func (mgr *Manager) Join(name string) (string, error) { + netHash := hashNetName(name) + netID := netHash[:8] + + mgr.mu.Lock() + if _, exists := mgr.networks[netID]; exists { + mgr.mu.Unlock() + return netID, nil + } + mgr.mu.Unlock() + + // Derive a network-specific identity. + netID_full := netID + derived, err := crypto.DeriveForNetwork(mgr.cfg.MasterIdentity, netHash) + if err != nil { + return "", fmt.Errorf("derive identity for %q: %w", name, err) + } + + // Open per-network store. + dbPath := filepath.Join(mgr.cfg.StoreDir, "messages-"+netID_full+".db") + st, err := store.Open(dbPath) + if err != nil { + return "", fmt.Errorf("open store for %q: %w", name, err) + } + + m := mesh.New(derived, st) + if mgr.cfg.ShareDir != "" { + m.ShareDir = mgr.cfg.ShareDir + } + + // Forward all mesh events to the Manager's fan-out, tagging with network_id. + meshEvents := m.Subscribe() + go func() { + for evt := range meshEvents { + evt.NetworkID = netID + mgr.emit(evt) + } + }() + + ctx, cancel := context.WithCancel(context.Background()) + + net := &Network{ + ID: netID, + Name: name, + Hash: netHash, + Identity: derived, + Mesh: m, + Store: st, + cancel: cancel, + } + + mgr.mu.Lock() + mgr.networks[netID] = net + mgr.order = append(mgr.order, netID) + mgr.mu.Unlock() + + log.Printf("netmgr: joining network %q (id=%s peer=%s)", name, netID, derived.PeerID().Short()) + + if mgr.cfg.AnchorURL != "" { + go func() { + anchor.Run(ctx, mgr.cfg.AnchorURL, name, derived, m) + log.Printf("netmgr: left network %q", name) + }() + } + + mgr.emit(proto.IpcMessage{ + Type: proto.EvtNetworkJoined, + NetworkID: netID, + NetworkName: name, + LocalPeer: peerPtr(derived.PeerInfo()), + }) + + return netID, nil +} + +// Leave cancels a network context by ID. Closes its store. +func (mgr *Manager) Leave(netID string) { + mgr.mu.Lock() + net, ok := mgr.networks[netID] + if ok { + delete(mgr.networks, netID) + mgr.order = removeStr(mgr.order, netID) + } + mgr.mu.Unlock() + + if !ok { + return + } + net.cancel() + net.Store.Close() + net.Mesh.Unsubscribe(net.Mesh.Subscribe()) // drain + close subscription + log.Printf("netmgr: left network %q (id=%s)", net.Name, netID) + mgr.emit(proto.IpcMessage{Type: proto.EvtNetworkLeft, NetworkID: netID}) +} + +// LeaveAll leaves every joined network. +func (mgr *Manager) LeaveAll() { + mgr.mu.RLock() + ids := make([]string, len(mgr.order)) + copy(ids, mgr.order) + mgr.mu.RUnlock() + for _, id := range ids { + mgr.Leave(id) + } +} + +// Get returns a network by ID. +func (mgr *Manager) Get(netID string) (*Network, bool) { + mgr.mu.RLock() + defer mgr.mu.RUnlock() + n, ok := mgr.networks[netID] + return n, ok +} + +// Default returns the first joined network, or nil if none are joined. +// Used to route commands that carry no network_id (backward compat). +func (mgr *Manager) Default() *Network { + mgr.mu.RLock() + defer mgr.mu.RUnlock() + if len(mgr.order) == 0 { + return nil + } + return mgr.networks[mgr.order[0]] +} + +// Resolve returns the network for netID, or the default if netID is empty. +func (mgr *Manager) Resolve(netID string) *Network { + if netID == "" { + return mgr.Default() + } + n, _ := mgr.Get(netID) + return n +} + +// All returns a snapshot of all joined networks in join order. +func (mgr *Manager) All() []*Network { + mgr.mu.RLock() + defer mgr.mu.RUnlock() + out := make([]*Network, 0, len(mgr.order)) + for _, id := range mgr.order { + out = append(out, mgr.networks[id]) + } + return out +} + +// AnchorURL returns the configured anchor URL. +func (mgr *Manager) AnchorURL() string { return mgr.cfg.AnchorURL } + +// Subscribe returns a channel that receives tagged events from all networks. +func (mgr *Manager) Subscribe() <-chan proto.IpcMessage { + ch := make(chan proto.IpcMessage, 128) + mgr.subsMu.Lock() + mgr.subs = append(mgr.subs, ch) + mgr.subsMu.Unlock() + return ch +} + +// Unsubscribe removes and closes a subscription channel. +func (mgr *Manager) Unsubscribe(ch <-chan proto.IpcMessage) { + mgr.subsMu.Lock() + defer mgr.subsMu.Unlock() + for i, s := range mgr.subs { + if s == ch { + mgr.subs = append(mgr.subs[:i], mgr.subs[i+1:]...) + close(s) + return + } + } +} + +func (mgr *Manager) emit(msg proto.IpcMessage) { + mgr.subsMu.Lock() + defer mgr.subsMu.Unlock() + for _, ch := range mgr.subs { + select { + case ch <- msg: + default: + } + } +} + +// ── helpers ─────────────────────────────────────────────────────────────────── + +func hashNetName(name string) string { + h := sha256.Sum256([]byte("yaw2-net:" + name)) + return hex.EncodeToString(h[:]) +} + +func removeStr(ss []string, s string) []string { + out := ss[:0] + for _, v := range ss { + if v != s { + out = append(out, v) + } + } + return out +} + +func peerPtr(p proto.PeerInfo) *proto.PeerInfo { return &p } diff --git a/internal/proto/proto.go b/internal/proto/proto.go index 71bd553..4bbfad6 100644 --- a/internal/proto/proto.go +++ b/internal/proto/proto.go @@ -215,12 +215,25 @@ const ( EvtError IpcMsgType = "error" EvtInviteGenerated IpcMsgType = "invite_generated" EvtFileList IpcMsgType = "file_list" + EvtNetworkJoined IpcMsgType = "network_joined" + EvtNetworkLeft IpcMsgType = "network_left" ) +// NetworkInfo summarises one joined network for state_snapshot and network_joined events. +type NetworkInfo struct { + NetworkID string `json:"network_id"` + NetworkName string `json:"network_name"` + LocalPeer *PeerInfo `json:"local_peer,omitempty"` +} + // IpcMessage covers both commands and events. type IpcMessage struct { Type IpcMsgType `json:"type"` + // optional: scopes a command/event to a specific network. + // When absent, defaults to the first (or only) joined network. + NetworkID string `json:"network_id,omitempty"` + // send_message Room string `json:"room,omitempty"` To *PeerID `json:"to,omitempty"` @@ -241,9 +254,12 @@ type IpcMessage struct { TransferID string `json:"transfer_id,omitempty"` BytesReceived int64 `json:"bytes_received,omitempty"` TotalBytes int64 `json:"total_bytes,omitempty"` - LocalPeer *PeerInfo `json:"local_peer,omitempty"` - ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"` - Rooms []string `json:"rooms,omitempty"` + // state_snapshot fields (existing shape preserved for backward compat) + LocalPeer *PeerInfo `json:"local_peer,omitempty"` + ConnectedPeers []PeerInfo `json:"connected_peers,omitempty"` + Rooms []string `json:"rooms,omitempty"` + // multi-network: all joined networks (additive) + Networks []NetworkInfo `json:"networks,omitempty"` ErrorMessage string `json:"error_message,omitempty"` InviteString string `json:"invite,omitempty"` Files []FileEntry `json:"files,omitempty"` diff --git a/test-network.sh b/test-network.sh index d004b0d..eef1e83 100755 --- a/test-network.sh +++ b/test-network.sh @@ -319,7 +319,9 @@ echo -e "file listing" echo -e "${DIM}────────────────────────────────────────────────────────${RESET}" for peer_ipc in "$ALICE_IPC" "$BOB_IPC" "$CHARLIE_IPC"; do - peer_name=$([ "$peer_ipc" = "$ALICE_IPC" ] && echo "alice" || ([ "$peer_ipc" = "$BOB_IPC" ] && echo "bob" || echo "charlie")) + if [ "$peer_ipc" = "$ALICE_IPC" ]; then peer_name="alice" + elif [ "$peer_ipc" = "$BOB_IPC" ]; then peer_name="bob" + else peer_name="charlie"; fi result=$(echo '{"type":"get_file_list"}' \ | timeout 2 nc 127.0.0.1 "$peer_ipc" 2>/dev/null \ | grep '"type":"file_list"' | head -1)