Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/workflows/server-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ jobs:
cache: true
cache-dependency-path: server/go.sum

- name: Run CDP telemetry browser regressions
run: go test -race ./lib/cdpmonitor -run '^(TestNetworkCapture|TestTelemetryConnection)' -count=1 -v
working-directory: server
env:
KERNEL_CDPMONITOR_CHROME_E2E: "1"

- name: Compute short SHA for images
id: vars
shell: bash
Expand Down
11 changes: 9 additions & 2 deletions server/lib/browsersurface/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package browsersurface

import (
"encoding/json"
"strings"

"github.com/kernel/kernel-images/server/lib/cdpclient"
)

func (t *Tracker) handleProtocolEvent(message cdpclient.Message) {
if !t.trackLocations && strings.HasPrefix(message.Method, "Page.") {
t.publish(Event{Kind: EventProtocol, SessionID: message.SessionID, Message: message})
return
}
switch message.Method {
case "Target.targetCreated":
var event struct {
Expand All @@ -16,8 +21,10 @@ func (t *Tracker) handleProtocolEvent(message cdpclient.Message) {
switch event.TargetInfo.Type {
case "page":
t.trackPage(event.TargetInfo, true)
case "iframe":
t.trackFrameTarget(event.TargetInfo)
default:
if t.tracksTarget(event.TargetInfo.Type) {
t.trackNonPageTarget(event.TargetInfo)
}
}
}
case "Target.targetInfoChanged":
Expand Down
80 changes: 61 additions & 19 deletions server/lib/browsersurface/frames.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@ import (
"time"
)

// Shared/service workers and background pages can outlive their creating tab.
func inheritsParentLifetime(targetType string) bool {
return targetType == "iframe" || targetType == "worker"
}

func (t *Tracker) addSession(sessionID, parentSessionID string, target targetInfo) {
if target.Type != "page" && target.Type != "iframe" {
if !t.tracksTarget(target.Type) {
return
}
t.stateMu.Lock()
if _, exists := t.sessions[sessionID]; exists {
t.stateMu.Unlock()
return
}
if inheritsParentLifetime(target.Type) && parentSessionID != "" && t.sessions[parentSessionID] == nil {
t.stateMu.Unlock()
return
}
tabID := t.tabsByTarget[target.TargetID]
if tabID == 0 && target.Type == "iframe" && parentSessionID != "" {
if parent := t.sessions[parentSessionID]; parent != nil {
Expand All @@ -33,14 +42,35 @@ func (t *Tracker) addSession(sessionID, parentSessionID string, target targetInf
}
}
ownedParentID := ""
if target.Type == "iframe" {
if inheritsParentLifetime(target.Type) {
ownedParentID = parentSessionID
}
t.sessions[sessionID] = &session{id: sessionID, parentID: ownedParentID, target: target, tabID: tabID}
t.bindSessionsLocked()
t.stateMu.Unlock()
t.signalChanged()
go t.initializeSession(sessionID)
t.publish(Event{
Kind: EventSessionAttached, SessionID: sessionID,
Target: SessionTarget{ID: target.TargetID, Type: target.Type, URL: target.URL, Title: target.Title, OpenerID: target.OpenerID, ParentFrameID: target.ParentFrameID},
})
if t.tracksTarget("worker") && target.Type != "service_worker" {
go t.attachDedicatedWorkers(sessionID)
}
if t.trackLocations && (target.Type == "page" || target.Type == "iframe") {
go t.initializeSession(sessionID)
}
}

func (t *Tracker) attachDedicatedWorkers(sessionID string) {
ctx, cancel := context.WithTimeout(t.ctx, sessionInitTimeout)
defer cancel()
_, err := t.protocol.Send(ctx, "Target.setAutoAttach", map[string]any{
"autoAttach": true, "flatten": true, "waitForDebuggerOnStart": false,
"filter": []map[string]any{{"type": "worker"}},
}, sessionID)
if err != nil && ctx.Err() == nil && t.SessionExists(sessionID) {
t.logger.Warn("failed to attach dedicated workers", "session_id", sessionID, "err", err)
}
}

func (t *Tracker) initializeSession(sessionID string) {
Expand Down Expand Up @@ -177,26 +207,38 @@ func (t *Tracker) upsertFrameLocked(tabID int, info frameInfo) {
}

func (t *Tracker) bindSessionsLocked() {
for _, sess := range t.sessions {
if sess.tabID != 0 {
continue
}
if tabID := t.tabsByTarget[sess.target.TargetID]; tabID != 0 {
sess.tabID = tabID
continue
}
if sess.target.Type == "iframe" {
if parent := t.sessions[sess.parentID]; parent != nil && parent.tabID != 0 {
sess.tabID = parent.tabID
for changed := true; changed; {
changed = false
for _, sess := range t.sessions {
if sess.target.Type != "page" && !inheritsParentLifetime(sess.target.Type) {
continue
}
if parentFrame := t.frames[sess.target.ParentFrameID]; parentFrame != nil {
sess.tabID = parentFrame.tabID
// Session-only tracking has no frame tree to associate descendants.
// Recover ownership without changing location-tracked initialization.
if !t.trackLocations && inheritsParentLifetime(sess.target.Type) && sess.parentID == "" {
for id, parent := range t.sessions {
if id != sess.id && parent.target.TargetID == sess.target.ParentFrameID {
sess.parentID = id
changed = true
break
}
}
}
if sess.tabID != 0 {
continue
}
if ownFrame := t.frames[sess.target.TargetID]; ownFrame != nil {
if tabID := t.tabsByTarget[sess.target.TargetID]; tabID != 0 {
sess.tabID = tabID
} else if parent := t.sessions[sess.parentID]; parent != nil {
sess.tabID = parent.tabID
Comment thread
rgarcia marked this conversation as resolved.
} else if parentFrame := t.frames[sess.target.ParentFrameID]; parentFrame != nil {
sess.tabID = parentFrame.tabID
} else if ownFrame := t.frames[sess.target.TargetID]; ownFrame != nil {
sess.tabID = ownFrame.tabID
}
if sess.tabID != 0 {
changed = true
}
}
}
}
Expand Down Expand Up @@ -290,8 +332,8 @@ func (t *Tracker) removeSessionLocked(sessionID string) []string {
switch sess.target.Type {
case "page":
t.trackingTarget[sess.target.TargetID] = false
case "iframe":
delete(t.trackingFrameTarget, sess.target.TargetID)
default:
delete(t.trackingNonPageTarget, sess.target.TargetID)
}
delete(t.sessions, id)
removed = append(removed, id)
Expand Down
34 changes: 34 additions & 0 deletions server/lib/browsersurface/iframe_initialization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package browsersurface

import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestExplicitIframeInitializationDoesNotWaitForParent(t *testing.T) {
tracker := New(newFakeProtocol())
defer tracker.stop()
page := targetInfo{TargetID: "page-a", Type: "page"}
tabID, _ := tracker.registerPage(page)
tracker.stateMu.Lock()
// The page's frame is known, but its session initialization is still pending.
tracker.sessions["session-a"] = &session{id: "session-a", target: page, tabID: tabID, initializing: true}
tracker.upsertFrameLocked(tabID, frameInfo{ID: "page-a"})
tracker.stateMu.Unlock()
tracker.addSession("oopif-session", "", targetInfo{
TargetID: "oopif", Type: "iframe", ParentFrameID: "page-a",
})
defer tracker.removeSession("oopif-session")

require.Eventually(t, func() bool {
tracker.stateMu.RLock()
defer tracker.stateMu.RUnlock()
sess := tracker.sessions["oopif-session"]
return sess != nil && sess.initialized
}, time.Second, 10*time.Millisecond)

tracker.removeSession("session-a")
require.True(t, tracker.SessionExists("oopif-session"), "an explicitly attached iframe initializes independently")
}
150 changes: 150 additions & 0 deletions server/lib/browsersurface/sessions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package browsersurface

import (
"context"
"maps"
"slices"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestWorkerDiscoveryIsOptIn(t *testing.T) {
for _, enabled := range []bool{false, true} {
for _, existing := range []bool{false, true} {
t.Run(testMode(enabled, existing), func(t *testing.T) {
protocol := newFakeProtocol()
targets := []targetInfo{
{TargetID: "worker", Type: "worker", URL: "https://example.com/worker.js"},
{TargetID: "shared_worker", Type: "shared_worker", URL: "https://example.com/shared.js"},
{TargetID: "service_worker", Type: "service_worker", URL: "https://example.com/service.js"},
}
protocol.targetInfos = make([]targetInfo, 0)
if existing {
protocol.targetInfos = targets
}
var options []Option
if enabled {
options = append(options, WithAdditionalTargets("worker", "shared_worker", "service_worker"))
}
tracker := New(protocol, options...)
events, unsubscribe := tracker.Subscribe()
defer unsubscribe()
defer close(protocol.events)
require.NoError(t, tracker.Start(context.Background()))
protocol.emitTarget("Target.attachedToTarget", map[string]any{
"sessionId": "worker-session", "targetInfo": targets[0],
})
for _, target := range targets {
// Repeated discovery must not create another session.
protocol.emitTarget("Target.targetCreated", map[string]any{"targetInfo": target})
}
attached := make(map[string]bool)
created := 0
timeout := time.After(time.Second)
for created != len(targets) || (enabled && len(attached) != len(targets)) {
select {
case event := <-events:
if event.Kind == EventSessionAttached {
require.True(t, enabled)
require.False(t, attached[event.Target.Type])
attached[event.Target.Type] = true
require.True(t, tracker.SessionExists(event.SessionID))
}
if event.Kind == EventProtocol && event.Message.Method == "Target.targetCreated" {
created++
}
case <-timeout:
t.Fatal("worker discovery events did not arrive")
}
}
if enabled {
require.Eventually(t, func() bool {
protocol.mu.Lock()
defer protocol.mu.Unlock()
return protocol.autoAttachCalls["worker-session"] == 1
}, time.Second, time.Millisecond, "nested dedicated-worker discovery must be enabled")
}
protocol.mu.Lock()
calls := maps.Clone(protocol.attachCalls)
types := slices.Clone(protocol.discoveredTypes)
pageCalls := maps.Clone(protocol.pageEnableCalls)
protocol.mu.Unlock()
for _, target := range targets {
if enabled {
if target.Type == "worker" {
require.Zero(t, calls[target.TargetID], "dedicated workers attach only through their parent")
} else {
require.Equal(t, 1, calls[target.TargetID])
}
require.Contains(t, types, target.Type)
} else {
require.Zero(t, calls[target.TargetID])
require.NotContains(t, types, target.Type)
}
}
require.Empty(t, pageCalls, "worker sessions must not initialize Page domains")
})
}
}
}

func testMode(enabled, existing bool) string {
mode := "default"
if enabled {
mode = "workers"
}
if existing {
return mode + "/existing"
}
return mode + "/new"
}

func TestSessionOnlyTrackingDoesNotWaitForLocations(t *testing.T) {
protocol := newFakeProtocol()
protocol.pageEnableFailures["session-a"] = 100
tracker := New(protocol, WithoutLocations(), WithAdditionalTargets("worker"))
events, unsubscribe := tracker.Subscribe()
defer unsubscribe()
defer close(protocol.events)
require.NoError(t, tracker.Start(context.Background()))

attached := 0
for attached != 2 {
select {
case event := <-events:
if event.Kind == EventSessionAttached {
attached++
}
case <-time.After(time.Second):
t.Fatal("page attachment waited for location initialization")
}
}
protocol.mu.Lock()
pageCalls := maps.Clone(protocol.pageEnableCalls)
protocol.mu.Unlock()
require.Empty(t, pageCalls)
require.Empty(t, tracker.Snapshot().Tabs)

// Children can arrive before their parents and have no Page frame tree.
tracker.addSession("nested", "", targetInfo{TargetID: "nested", Type: "iframe", ParentFrameID: "oopif"})
tracker.addSession("child", "", targetInfo{TargetID: "oopif", Type: "iframe", ParentFrameID: "page-a"})
tracker.removeTarget("page-a")
require.False(t, tracker.SessionExists("session-a"))
require.False(t, tracker.SessionExists("child"))
require.False(t, tracker.SessionExists("nested"))
require.True(t, tracker.SessionExists("session-b"))
tracker.addSession("late-worker", "session-a", targetInfo{TargetID: "worker", Type: "worker"})
require.False(t, tracker.SessionExists("late-worker"))
}

func TestLateAttachResponseDoesNotRetainClosedTarget(t *testing.T) {
tracker := New(newFakeProtocol(), WithoutLocations())
defer tracker.stop()
target := targetInfo{TargetID: "page-a", Type: "page", URL: "https://example.com/"}
tracker.registerPage(target)
tracker.removeTarget(target.TargetID)
require.NoError(t, tracker.attachTarget(target))
require.False(t, tracker.SessionExists("session-a"))
}
Loading
Loading