Probe known and Tailscale peers in headless send
runHeadlessSend only ran multicast discovery, so `--to <alias> --message` could not reach a peer that is only routable over the tailnet (or another subnet), even though the TUI and quick-send paths could via watchRemotes. Start the same watcher in the headless path: known peers from config plus online Tailscale peers are unicast-probed, the probe handshake records the peer, and FindPeer picks it up like any multicast discovery. Verified live against a tailnet-only peer (different subnet, DERP-relayed): discovered and message delivered. TestFindPeerViaProbe covers the composition Probe -> NotePeer -> PeerFound -> FindPeer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a15440ed20
commit
ff67bd6a71
3 changed files with 53 additions and 6 deletions
13
README.md
13
README.md
|
|
@ -111,11 +111,14 @@ omarchy-send --to "Strong Onion" --message "hi" --send-pin 2468 # if the peer
|
||||||
```
|
```
|
||||||
|
|
||||||
The target is matched against the peer's display name, case-insensitively. The
|
The target is matched against the peer's display name, case-insensitively. The
|
||||||
command discovers the peer over multicast (waiting up to `--wait`, default 15s),
|
command discovers the peer over multicast and, like the TUI, directly probes
|
||||||
sends the message, prints a one-line result, and exits non-zero if the peer
|
your known peers and online Tailscale peers (waiting up to `--wait`, default
|
||||||
isn't found or the send fails. It starts discovery only — not the receiver — so
|
15s) — so a remote box added with `+` in the TUI, or any tailnet peer, is a
|
||||||
it's safe to run while another `omarchy-send` instance is up. Both `--to` and
|
valid `--to` target from a script too. It sends the message, prints a one-line
|
||||||
`--message` are required; file sending stays in the TUI for now.
|
result, and exits non-zero if the peer isn't found or the send fails. It starts
|
||||||
|
discovery only — not the receiver — so it's safe to run while another
|
||||||
|
`omarchy-send` instance is up. Both `--to` and `--message` are required; file
|
||||||
|
sending stays in the TUI for now.
|
||||||
|
|
||||||
### Sending files
|
### Sending files
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,11 @@ func runHeadlessSend(cfg config.Config, target, message, sendPIN string, wait ti
|
||||||
}
|
}
|
||||||
disc.Announce() // solicit replies immediately rather than waiting a tick
|
disc.Announce() // solicit replies immediately rather than waiting a tick
|
||||||
|
|
||||||
|
// Multicast can't cross subnets or the tailnet, so also probe known peers
|
||||||
|
// and online Tailscale peers directly — same as the TUI's device list.
|
||||||
|
rem := &remotes{hosts: cfg.KnownPeers}
|
||||||
|
go watchRemotes(ctx, disc, rem)
|
||||||
|
|
||||||
want := strings.TrimSpace(target)
|
want := strings.TrimSpace(target)
|
||||||
fmt.Fprintf(os.Stderr, "Looking for %q on the network (up to %s)…\n", want, wait)
|
fmt.Fprintf(os.Stderr, "Looking for %q on the network (up to %s)…\n", want, wait)
|
||||||
|
|
||||||
|
|
@ -353,7 +358,7 @@ func runHeadlessSend(cfg config.Config, target, message, sendPIN string, wait ti
|
||||||
fmt.Fprintf(os.Stderr, " - %q (%s)\n", p.Info.Alias, p.IP)
|
fmt.Fprintf(os.Stderr, " - %q (%s)\n", p.Info.Alias, p.IP)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintln(os.Stderr, "No peers were seen at all — check you're on the same LAN and the target is running omarchy-send / LocalSend.")
|
fmt.Fprintln(os.Stderr, "No peers were seen at all — check the target is running omarchy-send / LocalSend on the same LAN, or is reachable as a known peer / over Tailscale.")
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"omarchy-send/internal/protocol"
|
"omarchy-send/internal/protocol"
|
||||||
)
|
)
|
||||||
|
|
@ -46,6 +47,44 @@ func TestProbeRegistersPeer(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFindPeerViaProbe covers the headless-send path for remote peers: a peer
|
||||||
|
// that multicast can't see (here: only reachable by unicast Probe) must still
|
||||||
|
// satisfy a FindPeer that is already waiting — Probe → NotePeer → PeerFound.
|
||||||
|
func TestFindPeerViaProbe(t *testing.T) {
|
||||||
|
peerInfo := protocol.DeviceInfo{Alias: "titan-box", Fingerprint: "titan-fp", Port: 53317, Protocol: "http"}
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/api/localsend/v2/register", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = json.NewEncoder(w).Encode(peerInfo)
|
||||||
|
})
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
defer srv.Close()
|
||||||
|
host := strings.TrimPrefix(srv.URL, "http://")
|
||||||
|
|
||||||
|
d := New(protocol.DeviceInfo{Fingerprint: "self-fp", Alias: "Self"})
|
||||||
|
|
||||||
|
// Probe concurrently, like watchRemotes does while FindPeer waits.
|
||||||
|
go func() {
|
||||||
|
if err := d.Probe(context.Background(), host); err != nil {
|
||||||
|
t.Errorf("Probe failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
got, err := d.FindPeer(ctx, func(p Peer) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(p.Info.Alias), "titan-box")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindPeer did not see the probed peer: %v", err)
|
||||||
|
}
|
||||||
|
if got.Info.Fingerprint != "titan-fp" {
|
||||||
|
t.Errorf("fingerprint = %q, want titan-fp", got.Info.Fingerprint)
|
||||||
|
}
|
||||||
|
if wantIP := strings.Split(host, ":")[0]; got.IP != wantIP {
|
||||||
|
t.Errorf("peer IP = %q, want %q (the host probed)", got.IP, wantIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProbeUnreachableErrors(t *testing.T) {
|
func TestProbeUnreachableErrors(t *testing.T) {
|
||||||
d := New(protocol.DeviceInfo{Fingerprint: "self-fp"})
|
d := New(protocol.DeviceInfo{Fingerprint: "self-fp"})
|
||||||
// 127.0.0.1:1 — nothing listening; both https and http should fail fast.
|
// 127.0.0.1:1 — nothing listening; both https and http should fail fast.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue