Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions cmd/wasm-visor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,16 @@ var (
ctx context.Context

selfPK cipher.PubKey
dmsgC *dmsg.Client
tpM *transport.Manager
rtr router.Router
procM appserver.ProcManager
hvCore *wasmhv.Core
tpd transport.DiscoveryClient
// bootTime is stamped when bootEdge starts, so SelfSummary/host-stats can
// report a real uptime instead of a hard-coded 0 (which read as a node that
// just started / is broken on the multi-visor page).
bootTime time.Time
dmsgC *dmsg.Client
tpM *transport.Manager
rtr router.Router
procM appserver.ProcManager
hvCore *wasmhv.Core
tpd transport.DiscoveryClient

// wsTable / wtTable hold the dial targets (peer PK → endpoint) for the
// browser-dialable direct transports. They are mutable so the dialTransport
Expand Down Expand Up @@ -257,6 +261,7 @@ func bootEdge(skHex, seedPKHex, seedWSURL, discDmsgAddr string) (cipher.PubKey,
return pk, err
}
selfPK = pk
bootTime = time.Now()

// Resolve the deployment service endpoints through the SHARED resolver
// (pkg/visor/visorcore) — the same one the native visor will use — so the two
Expand Down Expand Up @@ -1031,9 +1036,31 @@ func (s visorSelf) SelfSummary() wasmhv.Summary {
BuildTag: "wasm",
Online: true,
IsHypervisor: true,
// Mirror-struct fields the native SelfSummary populates but the wasm
// builder used to leave zero — so the node page rendered a real
// long-running tab as "0s uptime", autoconnect off, etc.:
// - Uptime: real seconds since bootEdge (0 read as just-started/broken).
// - PublicAutoconnect: true — the edge runs the WS/WebRTC autoconnect
// loop (autoconnect_js.go) by default.
// - IsPublic: false, stated explicitly — a browser edge accepts no
// inbound and never publishes a public entry.
// - MinHops: 0 is the edge's real setting (direct routing).
// - RewardAddress: genuinely none (no on-disk config; earns no rewards),
// left empty rather than faked.
Uptime: uptimeSeconds(),
PublicAutoconnect: true,
IsPublic: false,
}
}

// uptimeSeconds is the tab's real uptime; 0 before bootEdge stamps bootTime.
func uptimeSeconds() float64 {
if bootTime.IsZero() {
return 0
}
return time.Since(bootTime).Seconds()
}

// tpController is the wasmhv.TransportController backing the RPC gateway's
// transport-control methods (the CLI's `tp add`/`tp rm`), over this tab's
// transport.Manager. webrtc/dmsg dial by PK alone; ws/wt need an endpoint and
Expand Down
24 changes: 19 additions & 5 deletions pkg/wasmhv/self.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,16 @@ func (c *Core) selfRoute(self SelfProvider, method, sub string, body []byte, que
return jsonResp([]string{"dmsg", "swsr", "swtr", "webrtc"})
case "host-stats":
// A browser tab has no host to measure (no CPU/RAM/disk/NIC of its own),
// so report the shape with zeros + js/wasm identity. This keeps the
// multi-visor resources page from 404-erroring on a serverless visor; it
// renders the row with empty gauges rather than failing the whole page.
// so report the shape + js/wasm identity but flag the hardware metrics as
// unmeasurable via "available": false — the honest signal so the UI can
// render N/A instead of treating 0% CPU / 0-byte RAM as a real reading
// (which made a browser visor look idle-but-fine rather than
// not-applicable). This keeps the multi-visor resources page from
// 404-erroring on a serverless visor. The zeros remain for older UI that
// reads the numbers directly.
return jsonResp(map[string]interface{}{
"hostname": "browser", "os": "js", "platform": "wasm", "arch": "wasm",
"available": false,
"uptime_seconds": 0, "cpu_percent": 0, "cpu_count": 0, "cpu_logical_count": 0,
"mem_total": 0, "mem_used": 0, "mem_available": 0, "mem_percent": 0,
"disk_total": 0, "disk_used": 0, "disk_free": 0, "disk_percent": 0,
Expand Down Expand Up @@ -203,8 +208,17 @@ func (c *Core) selfRoute(self SelfProvider, method, sub string, body []byte, que
// single app; PUT applies status (start/stop) and/or autostart via the
// SelfProvider's in-process app control, then returns the fresh state.
func (c *Core) selfAppRoute(self SelfProvider, method, appName string, body []byte) (int, []byte) {
// Only the bare app name; apps/<app>/logs|stats|connections aren't wired for
// the self visor (a browser tab has no per-app BoltDB log). Strip the tail.
// apps/<app>/logs: a browser tab has no per-app BoltDB log store, but the
// Angular app-detail page requests /logs and treats a 404 as an error
// dialog. Serve a valid, EMPTY LogsRes (the native shape) so the logs panel
// renders "no logs" cleanly instead of erroring — the same
// render-cleanly-don't-404 treatment routegroups/host-stats already get.
// (Richer in-process app logs on the wasm edge are a separate follow-up.)
if parts := strings.SplitN(appName, "/", 2); len(parts) == 2 && parts[1] == "logs" {
return jsonResp(map[string]interface{}{"last_log_timestamp": "", "logs": []string{}})
}
// Only the bare app name for the remaining GET/PUT; stats|connections aren't
// wired for the self visor either — strip the tail.
appName = strings.SplitN(appName, "/", 2)[0]
if method == "PUT" {
var req struct {
Expand Down
43 changes: 43 additions & 0 deletions pkg/wasmhv/self_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,46 @@ func TestSelf_DmsgConnectAllRoute(t *testing.T) {
t.Fatalf("GET dmsg/connect-all status = %d, want 405", status)
}
}

// TestSelf_AppLogsServesEmptyNot404 locks the fix that a self-app /logs request
// returns a valid (empty) LogsRes instead of the generic subroute 404 — so the
// Angular app-detail logs panel renders "no logs" instead of an error dialog.
func TestSelf_AppLogsServesEmptyNot404(t *testing.T) {
pk := newSelfPK(t)
core := NewCore(pk, nil)
core.SetSelf(fakeSelf{pk: pk})

status, body := core.ServeHTTP("GET", "/api/visors/"+pk.Hex()+"/apps/skychat/logs", nil)
if status != 200 {
t.Fatalf("apps/<app>/logs status = %d (body %s), want 200", status, body)
}
var res struct {
LastLogTimestamp string `json:"last_log_timestamp"`
Logs []string `json:"logs"`
}
if err := json.Unmarshal(body, &res); err != nil {
t.Fatalf("app logs body not a LogsRes: %v (%s)", err, body)
}
if res.Logs == nil {
t.Fatalf("logs should be an empty array, not null: %s", body)
}
}

// TestSelf_HostStatsFlagsUnavailable locks host-stats reporting available:false
// (a browser tab can't measure its host — the UI must show N/A, not 0%).
func TestSelf_HostStatsFlagsUnavailable(t *testing.T) {
pk := newSelfPK(t)
core := NewCore(pk, nil)
core.SetSelf(fakeSelf{pk: pk})

status, body := core.ServeHTTP("GET", "/api/visors/"+pk.Hex()+"/host-stats", nil)
if status != 200 {
t.Fatalf("host-stats status = %d", status)
}
var res struct {
Available *bool `json:"available"`
}
if err := json.Unmarshal(body, &res); err != nil || res.Available == nil || *res.Available {
t.Fatalf("host-stats must report available:false, got %s", body)
}
}
Loading