Skip to content

Commit

Permalink
Add Tab type and use it when we want to refer to single client instances
Browse files Browse the repository at this point in the history
  • Loading branch information
kegsay committed Jul 8, 2024
1 parent 92b76a2 commit a72753e
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 101 deletions.
35 changes: 33 additions & 2 deletions internal/api/js/chrome/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"time"

"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
)

Expand All @@ -32,8 +34,6 @@ func GlobalBrowser() (*Browser, error) {
}

type Browser struct {
BaseURL string
Cancel func()
Ctx context.Context // topmost chromedp context
ctxCancel func()
execAllocCancel func()
Expand Down Expand Up @@ -75,3 +75,34 @@ func (b *Browser) Close() {
b.ctxCancel()
b.execAllocCancel()
}

func (b *Browser) NewTab(baseJSURL string, onConsoleLog func(s string)) (*Tab, error) {
tabCtx, closeTab := chromedp.NewContext(b.Ctx)
err := chromedp.Run(tabCtx,
chromedp.Navigate(baseJSURL),
)
if err != nil {
return nil, fmt.Errorf("NewTab: failed to navigate to %s: %s", baseJSURL, err)
}

// Listen for console logs for debugging AND to communicate live updates
chromedp.ListenTarget(tabCtx, func(ev interface{}) {
switch ev := ev.(type) {
case *runtime.EventConsoleAPICalled:
for _, arg := range ev.Args {
s, err := strconv.Unquote(string(arg.Value))
if err != nil {
s = string(arg.Value)
}
onConsoleLog(s)
}
}
})

return &Tab{
BaseURL: baseJSURL,
Ctx: tabCtx,
browser: b,
cancel: closeTab,
}, nil
}
77 changes: 15 additions & 62 deletions internal/api/js/chrome/chrome.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,13 @@ package chrome

import (
"context"
"embed"
"fmt"
"io/fs"
"net"
"net/http"
"strconv"
"sync"

"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/matrix-org/complement/ct"
)

//go:embed dist
var jsSDKDistDirectory embed.FS

// Void is a type which can be used when you want to run an async function without returning anything.
// It can stop large responses causing errors "Object reference chain is too long (-32000)"
// when we don't care about the response.
Expand Down Expand Up @@ -61,68 +52,30 @@ func MustRunAsyncFn[T any](t ct.TestLike, ctx context.Context, js string) *T {
return result
}

func RunHeadless(onConsoleLog func(s string), requiresPersistance bool, listenPort int) (*Browser, error) {
func RunHeadless(onConsoleLog func(s string), listenPort int) (*Tab, error) {
// make a Chrome browser
browser, err := GlobalBrowser()
if err != nil {
return nil, err
return nil, fmt.Errorf("GlobalBrowser: %s", err)
}

// Listen for console logs for debugging AND to communicate live updates
chromedp.ListenTarget(browser.Ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *runtime.EventConsoleAPICalled:
for _, arg := range ev.Args {
s, err := strconv.Unquote(string(arg.Value))
if err != nil {
s = string(arg.Value)
}
onConsoleLog(s)
}
}
// Host the JS SDK
baseJSURL, closeSDKInstance, err := NewJSSDKWebsite(JSSDKInstanceOpts{
Port: listenPort,
})

// strip /dist so /index.html loads correctly as does /assets/xxx.js
c, err := fs.Sub(jsSDKDistDirectory, "dist")
if err != nil {
return nil, fmt.Errorf("failed to strip /dist off JS SDK files: %s", err)
return nil, fmt.Errorf("failed to create new js sdk instance: %s", err)
}

baseJSURL := ""
// run js-sdk (need to run this as a web server to avoid CORS errors you'd otherwise get with file: URLs)
var wg sync.WaitGroup
wg.Add(1)
mux := &http.ServeMux{}
mux.Handle("/", http.FileServer(http.FS(c)))
srv := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", listenPort),
Handler: mux,
}
startServer := func() {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
panic(err)
}
baseJSURL = "http://" + ln.Addr().String()
fmt.Println("JS SDK listening on", baseJSURL)
wg.Done()
srv.Serve(ln)
fmt.Println("JS SDK closing webserver")
}
go startServer()
wg.Wait()

// navigate to the page
err = chromedp.Run(browser.Ctx,
chromedp.Navigate(baseJSURL),
)
// Make a tab
tab, err := browser.NewTab(baseJSURL, onConsoleLog)
if err != nil {
return nil, fmt.Errorf("failed to navigate to %s: %s", baseJSURL, err)
closeSDKInstance()
return nil, fmt.Errorf("failed to create new tab: %s", err)
}

browser.BaseURL = baseJSURL
browser.Cancel = func() {
browser.Close()
srv.Close()
}
return browser, nil
// when we close the tab, close the hosted files too
tab.SetCloseServer(closeSDKInstance)

return tab, nil
}
76 changes: 76 additions & 0 deletions internal/api/js/chrome/tab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package chrome

import (
"context"
"embed"
"fmt"
"io/fs"
"net"
"net/http"
"sync"
)

//go:embed dist
var jsSDKDistDirectory embed.FS

type JSSDKInstanceOpts struct {
// The specific port this instance should be hosted on.
// This is crucial for persistent storage which relies on a stable port number
// across restarts.
Port int
}

// NewJSSDKWebsite hosts the JS SDK HTML/JS on a random high-numbered port
// and runs a Go web server to serve those files.
func NewJSSDKWebsite(opts JSSDKInstanceOpts) (baseURL string, close func(), err error) {
// strip /dist so /index.html loads correctly as does /assets/xxx.js
c, err := fs.Sub(jsSDKDistDirectory, "dist")
if err != nil {
return "", nil, fmt.Errorf("failed to strip /dist off JS SDK files: %s", err)
}
// run js-sdk (need to run this as a web server to avoid CORS errors you'd otherwise get with file: URLs)
var wg sync.WaitGroup
wg.Add(1)
mux := &http.ServeMux{}
mux.Handle("/", http.FileServer(http.FS(c)))
srv := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", opts.Port),
Handler: mux,
}
startServer := func() {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
panic(err)
}
baseURL = "http://" + ln.Addr().String()
fmt.Println("JS SDK listening on", baseURL)
wg.Done()
srv.Serve(ln)
fmt.Println("JS SDK closing webserver")
}
go startServer()
wg.Wait()
return baseURL, func() {
srv.Close()
}, nil
}

// Tab represents an open JS SDK instance tab
type Tab struct {
BaseURL string
Ctx context.Context // tab context
browser *Browser // a ref to the browser which made this tab
closeServer func()
cancel func() // closes the tab
}

func (t *Tab) Close() {
t.cancel()
if t.closeServer != nil {
t.closeServer()
}
}

func (t *Tab) SetCloseServer(close func()) {
t.closeServer = close
}
Loading

0 comments on commit a72753e

Please sign in to comment.