openvibers/openvibe.link Commits
openvibe-node share <port>, and every share in openvibe-node status (CON-4) (#114)
Changes
@@@ -22,3 +22,7 @@ internal/memory/testdata/redact-corpus.json:private-key:6internal/memory/testdata/redact-corpus.json:private-key:30internal/memory/testdata/redact-corpus.json:private-key:42internal/memory/testdata/redact-corpus.json:private-key:474++++# The Connect status test lists a share by its port; the fake key it used to carry was removed, but the++# finding remains in this branch's first commit (a test literal, never a real key).++internal/connect/live_test.go:generic-api-key:43@@@ -28,13 +28,14 @@ openvibe.bot shows this command with the robot's id and a fresh code. `--driver`OpenVibe.Network gets `--network <URL> --pairing pair_… --code ABCD-1234` instead of `--robot`. Details, manualinstall, Windows and configuration: [docs/install.md](docs/install.md).--**Put a port on the web** with [OpenVibe Connect](https://connect.openvibe.website): `openvibe-node share --key ovc_… --port 8790`--serves it at `https://<name>--<you>.openvibe.website` behind OpenVibe sign-in, and `openvibe-node share add` keeps it--up with the service ([docs/connect.md](docs/connect.md)).++**Put a port on the web** with [OpenVibe Connect](https://connect.openvibe.website): `openvibe-node share 8790`++serves the share this computer was picked for at `https://<name>--<you>.openvibe.website` behind OpenVibe sign-in (or++`openvibe-node share --key ovc_… --port 8790` with a key), and `openvibe-node share add` keeps it up with the service;++`openvibe-node status` lists each share ([docs/connect.md](docs/connect.md)).```shopenvibe-node # guided first run: pair, check the link, report this machine's first job--openvibe-node status # link, plugins, video, battery, stop latch++openvibe-node status # link, plugins, video, battery, shares, stop latchopenvibe-node stop # kill switch: stop every motor now and keep it stoppedopenvibe-node resumeopenvibe-node quit # end the node cleanly (nothing latched), e.g. from a stream launcher@@@ -20,6 +20,7 @@"the live screen: the machine's screen on openvibe.bot for its owner only, up to 30 minutes","the job worker (--worker): sandboxed function, code and linux jobs, and the OpenVibe.Codes job runner","the machine's roles from the network's hub (codes.worker, desks.host, ai.host, bot.robot): read at check-in, kept, and applied to what the machine was installed for; the local configuration always wins",++ "OpenVibe Connect shares (CON-4): `openvibe-node share <port>` serves the share this computer was picked for, with no key; every saved share is in `openvibe-node status` (address, connected or why not, requests) and stops when the Node stops","one-command install on a Raspberry Pi or other Linux robot (I2C, SPI, camera tools, kit detection)","system service on systemd, launchd and Windows (or a Windows sign-in start)","ESP32 library (OpenVibeNode): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop"@@@ -808,6 +808,35 @@ func cmdAnswer(g *globals, cmd, id string, stdout io.Writer) error {return nil}++// sharesText lists the OpenVibe Connect shares this Node keeps up (CON-4). A share that is reconnecting says why; a++// key-served share is named by its port and never shows the key.++func sharesText(shares []connect.Status) string {++ if len(shares) == 0 {++ return ""++ }++ var b strings.Builder++ fmt.Fprintf(&b, "Shares: %d kept up\n", len(shares))++ for _, s := range shares {++ state := "not connected"++ if s.Connected {++ state = "connected"++ if s.Requests > 0 {++ state = fmt.Sprintf("connected, %d requests", s.Requests)++ }++ }++ where := s.URL++ if where == "" {++ where = "(address shown once it connects)"++ }++ fmt.Fprintf(&b, " localhost:%d %s %s", s.Port, where, state)++ if !s.Connected && s.LastError != "" {++ fmt.Fprintf(&b, " (%s)", s.LastError)++ }++ b.WriteByte('\n')++ }++ return b.String()++}++func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {resp, err := localctl.Call(g.paths.SocketPath(), localctl.Request{Cmd: localctl.CmdStatus}, 2*time.Second)if err != nil {@@@ -834,6 +863,7 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {link = "down: " + s.Link.LastError}fmt.Fprintf(stdout, "Link: %s, %d reconnects\n", link, s.Link.Reconnects)++ fmt.Fprint(stdout, sharesText(s.Shares))fmt.Fprintf(stdout, "Stop: %s\n", latchText(s.Latch))fmt.Fprintf(stdout, "Limits: speed %.2f, turn %.2f, command deadline ≤ %d ms\n", s.Limits.MaxSpeed, s.Limits.MaxTurn, s.Limits.MaxCommandMS)fmt.Fprintf(stdout, "Video: %s", s.Video.State)@@@ -7,6 +7,7 @@ import ("io""os""os/signal"++ "strconv""strings""syscall""time"@@@ -25,8 +26,10 @@ type shareArgs struct {}const shareUsage = `usage:-- openvibe-node share --share shr_… [--port N] serve a share this computer was picked for, until Ctrl-C (no key)-- openvibe-node share add --share shr_… [--port N] keep it up from the Node service++ openvibe-node share <port> serve the share this computer was picked for on that port, until Ctrl-C++ openvibe-node share add <port> keep that share up from the Node service++ openvibe-node share --share shr_… [--port N] serve one by its id, until Ctrl-C (no key)++ openvibe-node share add --share shr_… [--port N] keep one by its id up from the Node serviceopenvibe-node share mine the shares this computer was picked foropenvibe-node share --key ovc_… --port N serve a share with its connect key, until Ctrl-Copenvibe-node share add --key ovc_… --port N keep it up from the Node service (restart the service to start it)@@@ -87,6 +90,41 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, funreturn s, tokens, nil}++// shareByPort finds the one share the owner picked this computer for that names local port a.port (CON-4). It is the++// `openvibe-node share <port>` form: the machine's own node token buys the tunnel token, so there is no key.++func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {++ if a.port < 1 || a.port > 65535 {++ return connect.Share{}, nil, fmt.Errorf("port %d is not a port", a.port)++ }++ creds, err := credentials.Load(g.paths.CredentialFile(), nil)++ if err != nil || !creds.NetworkPaired() {++ return connect.Share{}, nil, errors.New("this computer is not added to OpenVibe yet: add it on openvibe.bot/machines, then pick it on the share's page")++ }++ tokens := connectNodeToken(creds)++ tok, err := tokens(ctx)++ if err != nil {++ return connect.Share{}, nil, err++ }++ mine, err := connect.NodeShares(ctx, nil, a.server, tok, "openvibe-node/"+version)++ if err != nil {++ return connect.Share{}, nil, err++ }++ var found []connect.NodeShare++ for _, m := range mine {++ if m.LocalPort == a.port {++ found = append(found, m)++ }++ }++ switch len(found) {++ case 0:++ return connect.Share{}, nil, fmt.Errorf("this computer does not serve localhost:%d: pick it on the share's page on connect.openvibe.website", a.port)++ case 1:++ return connect.Share{ID: found[0].ID, Port: found[0].LocalPort, Server: a.server, Name: found[0].Host}, tokens, nil++ default:++ return connect.Share{}, nil, fmt.Errorf("%d shares use localhost:%d; name one with --share shr_…", len(found), a.port)++ }++}++func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Writer) error {sub := ""if len(positional) > 0 {@@@ -95,11 +133,26 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wrif a.key == "" {a.key = os.Getenv("OV_CONNECT_KEY")}++ // `openvibe-node share <port>`: the share this computer was picked for, found by the local port its owner set on++ // the share's page (CON-4). The page gives this form because there is no key to copy.++ if a.share == "" && a.key == "" && a.port == 0 && len(positional) == 1 {++ if p, err := strconv.Atoi(positional[0]); err == nil && p > 0 && p < 65536 {++ a.port, positional, sub = p, nil, ""++ }++ }switch sub {case "", "run":if a.share != "" {return shareNow(g, a, stdout)}++ if a.key == "" && a.port != 0 {++ s, _, err := shareByPort(context.Background(), g, a)++ if err != nil {++ return err++ }++ a.share, a.port = s.ID, s.Port++ return shareNow(g, a, stdout)++ }if a.key == "" || a.port == 0 {fmt.Fprintln(stderr, shareUsage)return errUsage@@@ -128,6 +181,19 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr}return nilcase "add":++ if a.share == "" && a.key == "" && a.port == 0 && len(positional) == 2 {++ p, err := strconv.Atoi(positional[1])++ if err != nil || p < 1 || p > 65535 {++ fmt.Fprintln(stderr, shareUsage)++ return errUsage++ }++ a.port = p++ s, _, err := shareByPort(context.Background(), g, a)++ if err != nil {++ return err++ }++ a.share, a.port = s.ID, s.Port++ }if a.share != "" {if err := g.paths.Ensure(); err != nil {return err@@@ -7,6 +7,8 @@ import ("runtime""strings""testing"++++ "github.com/OpenVibers/OpenVibe.Node/internal/connect")// `openvibe-node share`: usage, add, list (the key only redacted), remove, and the file only the Node's account reads.@@@ -60,6 +62,8 @@ func TestShareByIDNeedsThePairing(t *testing.T) {for _, args := range [][]string{{"share", "--share", "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", "--home", home},{"share", "add", "--share", "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", "--port", "8790", "--home", home},++ {"share", "8790", "--home", home},++ {"share", "add", "8790", "--home", home},{"share", "mine", "--home", home},} {errb.Reset()@@@ -72,3 +76,22 @@ func TestShareByIDNeedsThePairing(t *testing.T) {t.Fatalf("a bad id: %d %q", code, errb.String())}}++++// `openvibe-node status` lists the shares the service keeps up (CON-4).++func TestSharesText(t *testing.T) {++ if got := sharesText(nil); got != "" {++ t.Fatalf("no shares: %q", got)++ }++ got := sharesText([]connect.Status{++ {ID: "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", Port: 8790, Host: "bot--alex.openvibe.website", URL: "https://bot--alex.openvibe.website", Connected: true, Requests: 3},++ {Port: 9001, LastError: "dial: connection refused"},++ })++ for _, want := range []string{"Shares: 2 kept up", "localhost:8790", "https://bot--alex.openvibe.website", "connected, 3 requests", "localhost:9001", "not connected", "dial: connection refused"} {++ if !strings.Contains(got, want) {++ t.Fatalf("status is missing %q:\n%s", want, got)++ }++ }++ if strings.Contains(got, "ovc_") {++ t.Fatalf("status printed a key:\n%s", got)++ }++}@@@ -12,9 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or serthen on the computer:```sh++openvibe-node share 8790 # serve the share this computer was picked for on that portopenvibe-node share mine # the shares this computer was picked foropenvibe-node share --share shr_… # serve one now, until Ctrl-C++openvibe-node share --share shr_… --port 8790 # the same, with the port namedsudo openvibe-node share add --share shr_… # or keep it up with the Node service++sudo openvibe-node share add 8790 # the same, found by its local port```There is no key to copy or leak. Before every connection the Node buys its node token for OpenVibe Connect (audience@@@ -52,10 +55,13 @@ openvibe-node share listopenvibe-node share remove 8790 # or the key, or the address```++`openvibe-node status` lists every share the service keeps up: its local port, its address on Connect, whether the++tunnel is connected (and why not), and how many requests the app has answered. A share with no address yet says so.++The saved shares are in the Node's config directory, `connect-shares.json`, readable only by the Node's account--(0600). The service starts every saved share when it starts, so restart it after `add` or `remove`. A share the server--ends for good (its key was replaced, it was closed, or another computer took it over) stays stopped and says so in the--log; `share list` shows each share's address once it has connected.++(0600). The service starts every saved share when it starts, and stops every one when the Node stops; restart it after++`add` or `remove`. A share the server ends for good (its key was replaced, it was closed, or another computer took it++over) stays stopped and says so in the log; `share list` shows each share's address once it has connected.## What the Node does and does not do@@@ -123,12 +123,30 @@ func Run(ctx context.Context, s Share, o Options) error {if o.MaxWait == 0 {o.MaxWait = 30 * time.Second}++ // The Node service's status lists the shares it keeps up (LiveShares). A foreground share has its own process,++ // so the service's status never shows it.++ markLive(s)++ defer forgetLive(s)++ userReady, userRequest := o.OnReady, o.OnRequest++ o.OnReady = func(r Ready) {++ liveReady(s, r)++ if userReady != nil {++ userReady(r)++ }++ }++ o.OnRequest = func(method, path string, status int, d time.Duration) {++ liveRequest(s)++ if userRequest != nil {++ userRequest(method, path, status, d)++ }++ }wait := o.MinWaitfor {err := runOnce(ctx, s, o, func() { wait = o.MinWait })if ctx.Err() != nil {return ctx.Err()}++ liveDown(s, err)if errors.Is(err, ErrStopped) {return err}nnew file mode 100644@@@ -0,0 +1,107 @@++package connect++++import (++ "fmt"++ "sort"++ "sync"++)++++// Status is one saved share's live state, as `openvibe-node status` (and its JSON) lists it. It never contains the++// key: a share served by this computer is named by its shr_… id, and a key-served share by its port.++type Status struct {++ ID string `json:"id,omitempty"`++ Port int `json:"port"`++ Host string `json:"host,omitempty"`++ URL string `json:"url,omitempty"`++ Connected bool `json:"connected"`++ Requests uint64 `json:"requests"`++ LastError string `json:"last_error,omitempty"`++}++++// live is the state of the shares this process keeps up. The Node service runs every saved share in this process++// (RunAll), and its local status socket answers from here; a foreground `openvibe-node share` has its own process and++// is not listed by the service's status.++var live = struct {++ sync.Mutex++ shares map[string]*Status++}{shares: map[string]*Status{}}++++// liveKey is the share's id, or its port for a share served with a connect key.++func liveKey(s Share) string {++ if s.ID != "" {++ return s.ID++ }++ return fmt.Sprintf("port:%d", s.Port)++}++++// markLive records a share that is about to connect (it stays listed while it reconnects).++func markLive(s Share) {++ live.Lock()++ defer live.Unlock()++ st := live.shares[liveKey(s)]++ if st == nil {++ st = &Status{ID: s.ID, Port: s.Port}++ live.shares[liveKey(s)] = st++ }++ st.Port = s.Port++ st.ID = s.ID++}++++// liveReady records the address the server gave and that the share is connected.++func liveReady(s Share, r Ready) {++ live.Lock()++ defer live.Unlock()++ st := live.shares[liveKey(s)]++ if st == nil {++ st = &Status{ID: s.ID, Port: s.Port}++ live.shares[liveKey(s)] = st++ }++ st.Host, st.URL, st.Connected, st.LastError = r.Host, r.URL, true, ""++}++++// liveDown records that the share is not connected, and why when it was not the owner stopping it.++func liveDown(s Share, err error) {++ live.Lock()++ defer live.Unlock()++ st := live.shares[liveKey(s)]++ if st == nil {++ return++ }++ st.Connected = false++ if err != nil {++ st.LastError = errString(err)++ }++}++++// liveRequest counts one request the share answered (or tried to).++func liveRequest(s Share) {++ live.Lock()++ defer live.Unlock()++ if st := live.shares[liveKey(s)]; st != nil {++ st.Requests++++ }++}++++// forgetLive drops a share when its runner ends, so a stopped share is not listed as still being served.++func forgetLive(s Share) {++ live.Lock()++ defer live.Unlock()++ delete(live.shares, liveKey(s))++}++++// LiveShares is a snapshot of the shares this process keeps up, by port then id.++func LiveShares() []Status {++ live.Lock()++ defer live.Unlock()++ out := make([]Status, 0, len(live.shares))++ for _, st := range live.shares {++ out = append(out, *st)++ }++ sort.Slice(out, func(i, j int) bool {++ if out[i].Port != out[j].Port {++ return out[i].Port < out[j].Port++ }++ return out[i].ID < out[j].ID++ })++ return out++}nnew file mode 100644@@@ -0,0 +1,95 @@++package connect++++import (++ "context"++ "errors"++ "strings"++ "testing"++ "time"++)++++// `openvibe-node status` lists the shares the service keeps up: their port, the address Connect gave, whether the++// tunnel is up, how many requests the app answered, and why it last went down.++func TestLiveSharesForStatus(t *testing.T) {++ s := Share{ID: "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", Port: 8790, Key: "ovc_hidden"}++ markLive(s)++ if got := LiveShares(); len(got) != 1 || got[0].Port != 8790 || got[0].Connected {++ t.Fatalf("marked: %+v", got)++ }++ liveReady(s, Ready{Host: "agents--alex.openvibe.website", URL: "https://agents--alex.openvibe.website"})++ liveRequest(s)++ liveRequest(s)++ got := LiveShares()++ if len(got) != 1 || !got[0].Connected || got[0].Host != "agents--alex.openvibe.website" ||++ got[0].URL != "https://agents--alex.openvibe.website" || got[0].Requests != 2 || got[0].LastError != "" {++ t.Fatalf("ready: %+v", got)++ }++ if got[0].ID != s.ID {++ t.Fatalf("the share id is listed: %+v", got)++ }++ liveDown(s, errors.New("dial: connection refused"))++ got = LiveShares()++ if got[0].Connected || got[0].LastError != "dial: connection refused" || got[0].Requests != 2 {++ t.Fatalf("down: %+v", got)++ }++ forgetLive(s)++ if got := LiveShares(); len(got) != 0 {++ t.Fatalf("forgotten: %+v", got)++ }++}++++// A share served with a key is listed by its port; a status never contains a key at all.++func TestLiveSharesHidesTheKey(t *testing.T) {++ s := Share{Port: 9001}++ markLive(s)++ got := LiveShares()++ if len(got) != 1 || got[0].ID != "" || got[0].Port != 9001 {++ t.Fatalf("listed: %+v", got)++ }++ if got[0].URL != "" || got[0].Host != "" {++ t.Fatalf("nothing to show yet: %+v", got)++ }++ forgetLive(s)++}++++// The service's shares are listed while it runs and stop when its context ends: `openvibe-node status` is the service++// process, and ending the Node ends every share it keeps up.++func TestRunAllListsAndStopsWithTheNode(t *testing.T) {++ f := newFakeServer(t, 0)++ dir := t.TempDir()++ if err := Add(dir, Share{Key: "ovc_" + strings.Repeat("L", 43), Port: 8790, Server: f.url()}); err != nil {++ t.Fatal(err)++ }++ ctx, cancel := context.WithCancel(context.Background())++ done := make(chan struct{})++ go func() {++ RunAll(ctx, dir, Options{})++ close(done)++ }()++ select {++ case <-f.conns:++ case <-time.After(3 * time.Second):++ cancel()++ t.Fatal("the share did not connect")++ }++ var got []Status++ for i := 0; i < 150; i++ {++ got = LiveShares()++ if len(got) == 1 && got[0].Connected {++ break++ }++ time.Sleep(20 * time.Millisecond)++ }++ if len(got) != 1 || !got[0].Connected || got[0].Port != 8790 {++ t.Fatalf("listed while running: %+v", got)++ }++ cancel()++ select {++ case <-done:++ case <-time.After(3 * time.Second):++ t.Fatal("RunAll did not return when the Node stopped")++ }++ if got := LiveShares(); len(got) != 0 {++ t.Fatalf("not listed after the Node stops: %+v", got)++ }++}@@@ -26,6 +26,7 @@ import ("time""github.com/OpenVibers/OpenVibe.Node/internal/config"++ "github.com/OpenVibers/OpenVibe.Node/internal/connect""github.com/OpenVibers/OpenVibe.Node/internal/credentials""github.com/OpenVibers/OpenVibe.Node/internal/firstrun""github.com/OpenVibers/OpenVibe.Node/internal/fleet"@@@ -1272,6 +1273,9 @@ type Status struct {// Roles are what the machine does for the network, as the owner set them on the hub (plan L34): the ids of the// catalogue. Empty when the owner set none.Roles []string `json:"roles,omitempty"`++ // Shares are the OpenVibe Connect shares this Node keeps up (CON-4): their port, address, whether the tunnel is++ // up and how many requests the app answered. Empty when the Node serves none.++ Shares []connect.Status `json:"shares,omitempty"`}type LinkStatus struct {@@@ -1328,6 +1332,7 @@ func (n *Node) Status() Status {}s.Screen = n.screenStatus()s.DeskStream = n.deskStreamStatus()++ s.Shares = connect.LiveShares()s.Video.State = video.StateOffif n.pub != nil {s.Video.State, s.Video.Frames, s.Video.LastError = n.pub.State(), n.pub.Frames(), n.pub.LastError()
@@ -22,3 +22,7 @@ internal/memory/testdata/redact-corpus.json:private-key:6
@@ -22,3 +22,7 @@ internal/memory/testdata/redact-corpus.json:private-key:6
internal/memory/testdata/redact-corpus.json:private-key:30
internal/memory/testdata/redact-corpus.json:private-key:30
internal/memory/testdata/redact-corpus.json:private-key:42
internal/memory/testdata/redact-corpus.json:private-key:42
internal/memory/testdata/redact-corpus.json:private-key:474
internal/memory/testdata/redact-corpus.json:private-key:474
+
+# The Connect status test lists a share by its port; the fake key it used to carry was removed, but the
+# finding remains in this branch's first commit (a test literal, never a real key).
+internal/connect/live_test.go:generic-api-key:43
@@ -28,13 +28,14 @@ openvibe.bot shows this command with the robot's id and a fresh code. `--driver`
@@ -28,13 +28,14 @@ openvibe.bot shows this command with the robot's id and a fresh code. `--driver`
OpenVibe.Network gets `--network <URL> --pairing pair_… --code ABCD-1234` instead of `--robot`. Details, manual
OpenVibe.Network gets `--network <URL> --pairing pair_… --code ABCD-1234` instead of `--robot`. Details, manual
install, Windows and configuration: [docs/install.md](docs/install.md).
install, Windows and configuration: [docs/install.md](docs/install.md).
-**Put a port on the web** with [OpenVibe Connect](https://connect.openvibe.website): `openvibe-node share --key ovc_… --port 8790`
-serves it at `https://<name>--<you>.openvibe.website` behind OpenVibe sign-in, and `openvibe-node share add` keeps it
-up with the service ([docs/connect.md](docs/connect.md)).
+**Put a port on the web** with [OpenVibe Connect](https://connect.openvibe.website): `openvibe-node share 8790`
+serves the share this computer was picked for at `https://<name>--<you>.openvibe.website` behind OpenVibe sign-in (or
+`openvibe-node share --key ovc_… --port 8790` with a key), and `openvibe-node share add` keeps it up with the service;
+`openvibe-node status` lists each share ([docs/connect.md](docs/connect.md)).
```sh
```sh
openvibe-node # guided first run: pair, check the link, report this machine's first job
openvibe-node # guided first run: pair, check the link, report this machine's first job
-openvibe-node status # link, plugins, video, battery, stop latch
+openvibe-node status # link, plugins, video, battery, shares, stop latch
openvibe-node stop # kill switch: stop every motor now and keep it stopped
openvibe-node stop # kill switch: stop every motor now and keep it stopped
openvibe-node resume
openvibe-node resume
openvibe-node quit # end the node cleanly (nothing latched), e.g. from a stream launcher
openvibe-node quit # end the node cleanly (nothing latched), e.g. from a stream launcher
@@ -20,6 +20,7 @@
@@ -20,6 +20,7 @@
"the live screen: the machine's screen on openvibe.bot for its owner only, up to 30 minutes",
"the live screen: the machine's screen on openvibe.bot for its owner only, up to 30 minutes",
"the job worker (--worker): sandboxed function, code and linux jobs, and the OpenVibe.Codes job runner",
"the job worker (--worker): sandboxed function, code and linux jobs, and the OpenVibe.Codes job runner",
"the machine's roles from the network's hub (codes.worker, desks.host, ai.host, bot.robot): read at check-in, kept, and applied to what the machine was installed for; the local configuration always wins",
"the machine's roles from the network's hub (codes.worker, desks.host, ai.host, bot.robot): read at check-in, kept, and applied to what the machine was installed for; the local configuration always wins",
+ "OpenVibe Connect shares (CON-4): `openvibe-node share <port>` serves the share this computer was picked for, with no key; every saved share is in `openvibe-node status` (address, connected or why not, requests) and stops when the Node stops",
"one-command install on a Raspberry Pi or other Linux robot (I2C, SPI, camera tools, kit detection)",
"one-command install on a Raspberry Pi or other Linux robot (I2C, SPI, camera tools, kit detection)",
"system service on systemd, launchd and Windows (or a Windows sign-in start)",
"system service on systemd, launchd and Windows (or a Windows sign-in start)",
"ESP32 library (OpenVibeNode): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop"
"ESP32 library (OpenVibeNode): pairing, control link, drive and actuator commands, telemetry, heartbeat, deadman and e-stop"
@@ -808,6 +808,35 @@ func cmdAnswer(g *globals, cmd, id string, stdout io.Writer) error {
@@ -808,6 +808,35 @@ func cmdAnswer(g *globals, cmd, id string, stdout io.Writer) error {
return nil
return nil
}
}
+// sharesText lists the OpenVibe Connect shares this Node keeps up (CON-4). A share that is reconnecting says why; a
+// key-served share is named by its port and never shows the key.
+func sharesText(shares []connect.Status) string {
+ if len(shares) == 0 {
+ return ""
+ }
+ var b strings.Builder
+ fmt.Fprintf(&b, "Shares: %d kept up\n", len(shares))
+ for _, s := range shares {
+ state := "not connected"
+ if s.Connected {
+ state = "connected"
+ if s.Requests > 0 {
+ state = fmt.Sprintf("connected, %d requests", s.Requests)
+ }
+ }
+ where := s.URL
+ if where == "" {
+ where = "(address shown once it connects)"
+ }
+ fmt.Fprintf(&b, " localhost:%d %s %s", s.Port, where, state)
+ if !s.Connected && s.LastError != "" {
+ fmt.Fprintf(&b, " (%s)", s.LastError)
+ }
+ b.WriteByte('\n')
+ }
+ return b.String()
+}
+
func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
resp, err := localctl.Call(g.paths.SocketPath(), localctl.Request{Cmd: localctl.CmdStatus}, 2*time.Second)
resp, err := localctl.Call(g.paths.SocketPath(), localctl.Request{Cmd: localctl.CmdStatus}, 2*time.Second)
if err != nil {
if err != nil {
@@ -834,6 +863,7 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
@@ -834,6 +863,7 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
link = "down: " + s.Link.LastError
link = "down: " + s.Link.LastError
}
}
fmt.Fprintf(stdout, "Link: %s, %d reconnects\n", link, s.Link.Reconnects)
fmt.Fprintf(stdout, "Link: %s, %d reconnects\n", link, s.Link.Reconnects)
+ fmt.Fprint(stdout, sharesText(s.Shares))
fmt.Fprintf(stdout, "Stop: %s\n", latchText(s.Latch))
fmt.Fprintf(stdout, "Stop: %s\n", latchText(s.Latch))
fmt.Fprintf(stdout, "Limits: speed %.2f, turn %.2f, command deadline ≤ %d ms\n", s.Limits.MaxSpeed, s.Limits.MaxTurn, s.Limits.MaxCommandMS)
fmt.Fprintf(stdout, "Limits: speed %.2f, turn %.2f, command deadline ≤ %d ms\n", s.Limits.MaxSpeed, s.Limits.MaxTurn, s.Limits.MaxCommandMS)
fmt.Fprintf(stdout, "Video: %s", s.Video.State)
fmt.Fprintf(stdout, "Video: %s", s.Video.State)
@@ -7,6 +7,7 @@ import (
@@ -7,6 +7,7 @@ import (
"io"
"io"
"os"
"os"
"os/signal"
"os/signal"
+ "strconv"
"strings"
"strings"
"syscall"
"syscall"
"time"
"time"
@@ -25,8 +26,10 @@ type shareArgs struct {
@@ -25,8 +26,10 @@ type shareArgs struct {
}
}
const shareUsage = `usage:
const shareUsage = `usage:
- openvibe-node share --share shr_… [--port N] serve a share this computer was picked for, until Ctrl-C (no key)
- openvibe-node share add --share shr_… [--port N] keep it up from the Node service
+ openvibe-node share <port> serve the share this computer was picked for on that port, until Ctrl-C
+ openvibe-node share add <port> keep that share up from the Node service
+ openvibe-node share --share shr_… [--port N] serve one by its id, until Ctrl-C (no key)
+ openvibe-node share add --share shr_… [--port N] keep one by its id up from the Node service
openvibe-node share mine the shares this computer was picked for
openvibe-node share mine the shares this computer was picked for
openvibe-node share --key ovc_… --port N serve a share with its connect key, until Ctrl-C
openvibe-node share --key ovc_… --port N serve a share with its connect key, until Ctrl-C
openvibe-node share add --key ovc_… --port N keep it up from the Node service (restart the service to start it)
openvibe-node share add --key ovc_… --port N keep it up from the Node service (restart the service to start it)
@@ -87,6 +90,41 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, fun
@@ -87,6 +90,41 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, fun
return s, tokens, nil
return s, tokens, nil
}
}
+// shareByPort finds the one share the owner picked this computer for that names local port a.port (CON-4). It is the
+// `openvibe-node share <port>` form: the machine's own node token buys the tunnel token, so there is no key.
+func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {
+ if a.port < 1 || a.port > 65535 {
+ return connect.Share{}, nil, fmt.Errorf("port %d is not a port", a.port)
+ }
+ creds, err := credentials.Load(g.paths.CredentialFile(), nil)
+ if err != nil || !creds.NetworkPaired() {
+ return connect.Share{}, nil, errors.New("this computer is not added to OpenVibe yet: add it on openvibe.bot/machines, then pick it on the share's page")
+ }
+ tokens := connectNodeToken(creds)
+ tok, err := tokens(ctx)
+ if err != nil {
+ return connect.Share{}, nil, err
+ }
+ mine, err := connect.NodeShares(ctx, nil, a.server, tok, "openvibe-node/"+version)
+ if err != nil {
+ return connect.Share{}, nil, err
+ }
+ var found []connect.NodeShare
+ for _, m := range mine {
+ if m.LocalPort == a.port {
+ found = append(found, m)
+ }
+ }
+ switch len(found) {
+ case 0:
+ return connect.Share{}, nil, fmt.Errorf("this computer does not serve localhost:%d: pick it on the share's page on connect.openvibe.website", a.port)
+ case 1:
+ return connect.Share{ID: found[0].ID, Port: found[0].LocalPort, Server: a.server, Name: found[0].Host}, tokens, nil
+ default:
+ return connect.Share{}, nil, fmt.Errorf("%d shares use localhost:%d; name one with --share shr_…", len(found), a.port)
+ }
+}
+
func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Writer) error {
func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Writer) error {
sub := ""
sub := ""
if len(positional) > 0 {
if len(positional) > 0 {
@@ -95,11 +133,26 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
@@ -95,11 +133,26 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
if a.key == "" {
if a.key == "" {
a.key = os.Getenv("OV_CONNECT_KEY")
a.key = os.Getenv("OV_CONNECT_KEY")
}
}
+ // `openvibe-node share <port>`: the share this computer was picked for, found by the local port its owner set on
+ // the share's page (CON-4). The page gives this form because there is no key to copy.
+ if a.share == "" && a.key == "" && a.port == 0 && len(positional) == 1 {
+ if p, err := strconv.Atoi(positional[0]); err == nil && p > 0 && p < 65536 {
+ a.port, positional, sub = p, nil, ""
+ }
+ }
switch sub {
switch sub {
case "", "run":
case "", "run":
if a.share != "" {
if a.share != "" {
return shareNow(g, a, stdout)
return shareNow(g, a, stdout)
}
}
+ if a.key == "" && a.port != 0 {
+ s, _, err := shareByPort(context.Background(), g, a)
+ if err != nil {
+ return err
+ }
+ a.share, a.port = s.ID, s.Port
+ return shareNow(g, a, stdout)
+ }
if a.key == "" || a.port == 0 {
if a.key == "" || a.port == 0 {
fmt.Fprintln(stderr, shareUsage)
fmt.Fprintln(stderr, shareUsage)
return errUsage
return errUsage
@@ -128,6 +181,19 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
@@ -128,6 +181,19 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
}
}
return nil
return nil
case "add":
case "add":
+ if a.share == "" && a.key == "" && a.port == 0 && len(positional) == 2 {
+ p, err := strconv.Atoi(positional[1])
+ if err != nil || p < 1 || p > 65535 {
+ fmt.Fprintln(stderr, shareUsage)
+ return errUsage
+ }
+ a.port = p
+ s, _, err := shareByPort(context.Background(), g, a)
+ if err != nil {
+ return err
+ }
+ a.share, a.port = s.ID, s.Port
+ }
if a.share != "" {
if a.share != "" {
if err := g.paths.Ensure(); err != nil {
if err := g.paths.Ensure(); err != nil {
return err
return err
@@ -7,6 +7,8 @@ import (
@@ -7,6 +7,8 @@ import (
"runtime"
"runtime"
"strings"
"strings"
"testing"
"testing"
+
+ "github.com/OpenVibers/OpenVibe.Node/internal/connect"
)
)
// `openvibe-node share`: usage, add, list (the key only redacted), remove, and the file only the Node's account reads.
// `openvibe-node share`: usage, add, list (the key only redacted), remove, and the file only the Node's account reads.
@@ -60,6 +62,8 @@ func TestShareByIDNeedsThePairing(t *testing.T) {
@@ -60,6 +62,8 @@ func TestShareByIDNeedsThePairing(t *testing.T) {
for _, args := range [][]string{
for _, args := range [][]string{
{"share", "--share", "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", "--home", home},
{"share", "--share", "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", "--home", home},
{"share", "add", "--share", "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", "--port", "8790", "--home", home},
{"share", "add", "--share", "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", "--port", "8790", "--home", home},
+ {"share", "8790", "--home", home},
+ {"share", "add", "8790", "--home", home},
{"share", "mine", "--home", home},
{"share", "mine", "--home", home},
} {
} {
errb.Reset()
errb.Reset()
@@ -72,3 +76,22 @@ func TestShareByIDNeedsThePairing(t *testing.T) {
@@ -72,3 +76,22 @@ func TestShareByIDNeedsThePairing(t *testing.T) {
t.Fatalf("a bad id: %d %q", code, errb.String())
t.Fatalf("a bad id: %d %q", code, errb.String())
}
}
}
}
+
+// `openvibe-node status` lists the shares the service keeps up (CON-4).
+func TestSharesText(t *testing.T) {
+ if got := sharesText(nil); got != "" {
+ t.Fatalf("no shares: %q", got)
+ }
+ got := sharesText([]connect.Status{
+ {ID: "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", Port: 8790, Host: "bot--alex.openvibe.website", URL: "https://bot--alex.openvibe.website", Connected: true, Requests: 3},
+ {Port: 9001, LastError: "dial: connection refused"},
+ })
+ for _, want := range []string{"Shares: 2 kept up", "localhost:8790", "https://bot--alex.openvibe.website", "connected, 3 requests", "localhost:9001", "not connected", "dial: connection refused"} {
+ if !strings.Contains(got, want) {
+ t.Fatalf("status is missing %q:\n%s", want, got)
+ }
+ }
+ if strings.Contains(got, "ovc_") {
+ t.Fatalf("status printed a key:\n%s", got)
+ }
+}
@@ -12,9 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or ser
@@ -12,9 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or ser
then on the computer:
then on the computer:
```sh
```sh
+openvibe-node share 8790 # serve the share this computer was picked for on that port
openvibe-node share mine # the shares this computer was picked for
openvibe-node share mine # the shares this computer was picked for
openvibe-node share --share shr_… # serve one now, until Ctrl-C
openvibe-node share --share shr_… # serve one now, until Ctrl-C
+openvibe-node share --share shr_… --port 8790 # the same, with the port named
sudo openvibe-node share add --share shr_… # or keep it up with the Node service
sudo openvibe-node share add --share shr_… # or keep it up with the Node service
+sudo openvibe-node share add 8790 # the same, found by its local port
```
```
There is no key to copy or leak. Before every connection the Node buys its node token for OpenVibe Connect (audience
There is no key to copy or leak. Before every connection the Node buys its node token for OpenVibe Connect (audience
@@ -52,10 +55,13 @@ openvibe-node share list
@@ -52,10 +55,13 @@ openvibe-node share list
openvibe-node share remove 8790 # or the key, or the address
openvibe-node share remove 8790 # or the key, or the address
```
```
+`openvibe-node status` lists every share the service keeps up: its local port, its address on Connect, whether the
+tunnel is connected (and why not), and how many requests the app has answered. A share with no address yet says so.
+
The saved shares are in the Node's config directory, `connect-shares.json`, readable only by the Node's account
The saved shares are in the Node's config directory, `connect-shares.json`, readable only by the Node's account
-(0600). The service starts every saved share when it starts, so restart it after `add` or `remove`. A share the server
-ends for good (its key was replaced, it was closed, or another computer took it over) stays stopped and says so in the
-log; `share list` shows each share's address once it has connected.
+(0600). The service starts every saved share when it starts, and stops every one when the Node stops; restart it after
+`add` or `remove`. A share the server ends for good (its key was replaced, it was closed, or another computer took it
+over) stays stopped and says so in the log; `share list` shows each share's address once it has connected.
## What the Node does and does not do
## What the Node does and does not do
@@ -123,12 +123,30 @@ func Run(ctx context.Context, s Share, o Options) error {
@@ -123,12 +123,30 @@ func Run(ctx context.Context, s Share, o Options) error {
if o.MaxWait == 0 {
if o.MaxWait == 0 {
o.MaxWait = 30 * time.Second
o.MaxWait = 30 * time.Second
}
}
+ // The Node service's status lists the shares it keeps up (LiveShares). A foreground share has its own process,
+ // so the service's status never shows it.
+ markLive(s)
+ defer forgetLive(s)
+ userReady, userRequest := o.OnReady, o.OnRequest
+ o.OnReady = func(r Ready) {
+ liveReady(s, r)
+ if userReady != nil {
+ userReady(r)
+ }
+ }
+ o.OnRequest = func(method, path string, status int, d time.Duration) {
+ liveRequest(s)
+ if userRequest != nil {
+ userRequest(method, path, status, d)
+ }
+ }
wait := o.MinWait
wait := o.MinWait
for {
for {
err := runOnce(ctx, s, o, func() { wait = o.MinWait })
err := runOnce(ctx, s, o, func() { wait = o.MinWait })
if ctx.Err() != nil {
if ctx.Err() != nil {
return ctx.Err()
return ctx.Err()
}
}
+ liveDown(s, err)
if errors.Is(err, ErrStopped) {
if errors.Is(err, ErrStopped) {
return err
return err
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
+package connect
+
+import (
+ "fmt"
+ "sort"
+ "sync"
+)
+
+// Status is one saved share's live state, as `openvibe-node status` (and its JSON) lists it. It never contains the
+// key: a share served by this computer is named by its shr_… id, and a key-served share by its port.
+type Status struct {
+ ID string `json:"id,omitempty"`
+ Port int `json:"port"`
+ Host string `json:"host,omitempty"`
+ URL string `json:"url,omitempty"`
+ Connected bool `json:"connected"`
+ Requests uint64 `json:"requests"`
+ LastError string `json:"last_error,omitempty"`
+}
+
+// live is the state of the shares this process keeps up. The Node service runs every saved share in this process
+// (RunAll), and its local status socket answers from here; a foreground `openvibe-node share` has its own process and
+// is not listed by the service's status.
+var live = struct {
+ sync.Mutex
+ shares map[string]*Status
+}{shares: map[string]*Status{}}
+
+// liveKey is the share's id, or its port for a share served with a connect key.
+func liveKey(s Share) string {
+ if s.ID != "" {
+ return s.ID
+ }
+ return fmt.Sprintf("port:%d", s.Port)
+}
+
+// markLive records a share that is about to connect (it stays listed while it reconnects).
+func markLive(s Share) {
+ live.Lock()
+ defer live.Unlock()
+ st := live.shares[liveKey(s)]
+ if st == nil {
+ st = &Status{ID: s.ID, Port: s.Port}
+ live.shares[liveKey(s)] = st
+ }
+ st.Port = s.Port
+ st.ID = s.ID
+}
+
+// liveReady records the address the server gave and that the share is connected.
+func liveReady(s Share, r Ready) {
+ live.Lock()
+ defer live.Unlock()
+ st := live.shares[liveKey(s)]
+ if st == nil {
+ st = &Status{ID: s.ID, Port: s.Port}
+ live.shares[liveKey(s)] = st
+ }
+ st.Host, st.URL, st.Connected, st.LastError = r.Host, r.URL, true, ""
+}
+
+// liveDown records that the share is not connected, and why when it was not the owner stopping it.
+func liveDown(s Share, err error) {
+ live.Lock()
+ defer live.Unlock()
+ st := live.shares[liveKey(s)]
+ if st == nil {
+ return
+ }
+ st.Connected = false
+ if err != nil {
+ st.LastError = errString(err)
+ }
+}
+
+// liveRequest counts one request the share answered (or tried to).
+func liveRequest(s Share) {
+ live.Lock()
+ defer live.Unlock()
+ if st := live.shares[liveKey(s)]; st != nil {
+ st.Requests++
+ }
+}
+
+// forgetLive drops a share when its runner ends, so a stopped share is not listed as still being served.
+func forgetLive(s Share) {
+ live.Lock()
+ defer live.Unlock()
+ delete(live.shares, liveKey(s))
+}
+
+// LiveShares is a snapshot of the shares this process keeps up, by port then id.
+func LiveShares() []Status {
+ live.Lock()
+ defer live.Unlock()
+ out := make([]Status, 0, len(live.shares))
+ for _, st := range live.shares {
+ out = append(out, *st)
+ }
+ sort.Slice(out, func(i, j int) bool {
+ if out[i].Port != out[j].Port {
+ return out[i].Port < out[j].Port
+ }
+ return out[i].ID < out[j].ID
+ })
+ return out
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
+package connect
+
+import (
+ "context"
+ "errors"
+ "strings"
+ "testing"
+ "time"
+)
+
+// `openvibe-node status` lists the shares the service keeps up: their port, the address Connect gave, whether the
+// tunnel is up, how many requests the app answered, and why it last went down.
+func TestLiveSharesForStatus(t *testing.T) {
+ s := Share{ID: "shr_01K9ZZZZZZZZZZZZZZZZZZZZZZ", Port: 8790, Key: "ovc_hidden"}
+ markLive(s)
+ if got := LiveShares(); len(got) != 1 || got[0].Port != 8790 || got[0].Connected {
+ t.Fatalf("marked: %+v", got)
+ }
+ liveReady(s, Ready{Host: "agents--alex.openvibe.website", URL: "https://agents--alex.openvibe.website"})
+ liveRequest(s)
+ liveRequest(s)
+ got := LiveShares()
+ if len(got) != 1 || !got[0].Connected || got[0].Host != "agents--alex.openvibe.website" ||
+ got[0].URL != "https://agents--alex.openvibe.website" || got[0].Requests != 2 || got[0].LastError != "" {
+ t.Fatalf("ready: %+v", got)
+ }
+ if got[0].ID != s.ID {
+ t.Fatalf("the share id is listed: %+v", got)
+ }
+ liveDown(s, errors.New("dial: connection refused"))
+ got = LiveShares()
+ if got[0].Connected || got[0].LastError != "dial: connection refused" || got[0].Requests != 2 {
+ t.Fatalf("down: %+v", got)
+ }
+ forgetLive(s)
+ if got := LiveShares(); len(got) != 0 {
+ t.Fatalf("forgotten: %+v", got)
+ }
+}
+
+// A share served with a key is listed by its port; a status never contains a key at all.
+func TestLiveSharesHidesTheKey(t *testing.T) {
+ s := Share{Port: 9001}
+ markLive(s)
+ got := LiveShares()
+ if len(got) != 1 || got[0].ID != "" || got[0].Port != 9001 {
+ t.Fatalf("listed: %+v", got)
+ }
+ if got[0].URL != "" || got[0].Host != "" {
+ t.Fatalf("nothing to show yet: %+v", got)
+ }
+ forgetLive(s)
+}
+
+// The service's shares are listed while it runs and stop when its context ends: `openvibe-node status` is the service
+// process, and ending the Node ends every share it keeps up.
+func TestRunAllListsAndStopsWithTheNode(t *testing.T) {
+ f := newFakeServer(t, 0)
+ dir := t.TempDir()
+ if err := Add(dir, Share{Key: "ovc_" + strings.Repeat("L", 43), Port: 8790, Server: f.url()}); err != nil {
+ t.Fatal(err)
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ done := make(chan struct{})
+ go func() {
+ RunAll(ctx, dir, Options{})
+ close(done)
+ }()
+ select {
+ case <-f.conns:
+ case <-time.After(3 * time.Second):
+ cancel()
+ t.Fatal("the share did not connect")
+ }
+ var got []Status
+ for i := 0; i < 150; i++ {
+ got = LiveShares()
+ if len(got) == 1 && got[0].Connected {
+ break
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ if len(got) != 1 || !got[0].Connected || got[0].Port != 8790 {
+ t.Fatalf("listed while running: %+v", got)
+ }
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(3 * time.Second):
+ t.Fatal("RunAll did not return when the Node stopped")
+ }
+ if got := LiveShares(); len(got) != 0 {
+ t.Fatalf("not listed after the Node stops: %+v", got)
+ }
+}
@@ -26,6 +26,7 @@ import (
@@ -26,6 +26,7 @@ import (
"time"
"time"
"github.com/OpenVibers/OpenVibe.Node/internal/config"
"github.com/OpenVibers/OpenVibe.Node/internal/config"
+ "github.com/OpenVibers/OpenVibe.Node/internal/connect"
"github.com/OpenVibers/OpenVibe.Node/internal/credentials"
"github.com/OpenVibers/OpenVibe.Node/internal/credentials"
"github.com/OpenVibers/OpenVibe.Node/internal/firstrun"
"github.com/OpenVibers/OpenVibe.Node/internal/firstrun"
"github.com/OpenVibers/OpenVibe.Node/internal/fleet"
"github.com/OpenVibers/OpenVibe.Node/internal/fleet"
@@ -1272,6 +1273,9 @@ type Status struct {
@@ -1272,6 +1273,9 @@ type Status struct {
// Roles are what the machine does for the network, as the owner set them on the hub (plan L34): the ids of the
// Roles are what the machine does for the network, as the owner set them on the hub (plan L34): the ids of the
// catalogue. Empty when the owner set none.
// catalogue. Empty when the owner set none.
Roles []string `json:"roles,omitempty"`
Roles []string `json:"roles,omitempty"`
+ // Shares are the OpenVibe Connect shares this Node keeps up (CON-4): their port, address, whether the tunnel is
+ // up and how many requests the app answered. Empty when the Node serves none.
+ Shares []connect.Status `json:"shares,omitempty"`
}
}
type LinkStatus struct {
type LinkStatus struct {
@@ -1328,6 +1332,7 @@ func (n *Node) Status() Status {
@@ -1328,6 +1332,7 @@ func (n *Node) Status() Status {
}
}
s.Screen = n.screenStatus()
s.Screen = n.screenStatus()
s.DeskStream = n.deskStreamStatus()
s.DeskStream = n.deskStreamStatus()
+ s.Shares = connect.LiveShares()
s.Video.State = video.StateOff
s.Video.State = video.StateOff
if n.pub != nil {
if n.pub != nil {
s.Video.State, s.Video.Frames, s.Video.LastError = n.pub.State(), n.pub.Frames(), n.pub.LastError()
s.Video.State, s.Video.Frames, s.Video.LastError = n.pub.State(), n.pub.Frames(), n.pub.LastError()