# Extension transport


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

``` python
from fastcore.test import test_eq
import httpx
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/ext.py#L22"
target="_blank" style="float:right; font-size:smaller">source</a>

### ExtChannel

``` python
def ExtChannel():
```

*Listen for the extension to dial in, then speak duplex JSON frames with
it*

Frames are JSON dicts, in four kinds. Commands carry a correlation `id`
plus `method`/`params` (and `tabId` for tab-scoped ones); the extension
answers each with `{id, result}` or `{id, error}`. Lifecycle *actions*
(`{id, action}`: `new-tab`, `attach`, `get-targets`, `detach`,
`close-tab`) cover operations `chrome.debugger` doesn’t expose as CDP.
Debugger events arrive as `{method, params, tabId}` with no `id`, and
keepalive pings have neither `id` nor `method` and are ignored by
consumers. `request` handles the correlation; anything that isn’t a
reply lands in `events`.

`token` is the trust check: the listener hands full browser control to
whoever dials in, so outside of throwaway settings the extension should
be configured with a shared secret, and only matching dials are
accepted.

To exercise everything without a browser, a scripted stand-in plays the
extension: it dials the listener and answers frames the way the real one
would, faking a single tab 99. Method frames are echoed back so tests
can see exactly what arrived, and enabling the
[`Page`](https://AnswerDotAI.github.io/fastcdp/core.html#page) domain
emits one debugger event, as the real extension does when a page loads.

``` python
async def fake_ext(url):
    "A stand-in extension: one fake tab 99, canned replies, an event on `Page.enable`"
    try:
        async with websockets.connect(url) as ws:
            async for frame in ws:
                m = json.loads(frame)
                if m.get('action')=='new-tab': r = dict(tabId=99)
                elif m.get('action')=='get-targets': r = [dict(type='page', tabId=99, url=m.get('url'))]
                elif m.get('action')=='close-tab': r = {}
                else: r = dict(method=m['method'], tabId=m.get('tabId'))
                await ws.send(json.dumps(dict(id=m['id'], result=r)))
                if m.get('method')=='Page.enable':
                    await ws.send(json.dumps(dict(method='Page.loadEventFired', params={}, tabId=m['tabId'])))
    except websockets.ConnectionClosed: pass
```

``` python
chan = await ExtChannel.listen(port=0, token='s3cret')
fake = asyncio.create_task(fake_ext(f'ws://localhost:{chan.port}/?token=s3cret'))
await chan.wait_peer(5)
assert chan.is_open
```

A dial with the wrong token is refused before any frame flows:

``` python
bad = await websockets.connect(f'ws://localhost:{chan.port}/?token=wrong')
try: await asyncio.wait_for(bad.recv(), 5); raise AssertionError('expected close')
except websockets.ConnectionClosed as e: test_eq(e.rcvd.code, 4003)
```

The listener also answers plain HTTP: the extension probes the port with
a cheap `fetch` before dialing the websocket, so a port with nothing
listening never spams its console with failed connections.

``` python
async with httpx.AsyncClient() as c: r = await c.get(f'http://localhost:{chan.port}/')
test_eq((r.status_code, r.text), (200, 'fastcdp\n'))
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/ext.py#L79"
target="_blank" style="float:right; font-size:smaller">source</a>

### ExtCDP

``` python
def ExtCDP(
    wsconn:NoneType=None, debug:bool=False
):
```

*CDP spoken with a companion extension, as JSON frames over a channel to
it*

`connect` wraps any connected channel; `listen` is the common case of
waiting for the extension locally. `_send` swaps fastcdp’s `sessionId`
for the extension’s `tabId` on the way out, and `_pump_events` swaps it
back on the way in, so every inherited helper that threads `sid` works
against tabs directly. `_action` sends the lifecycle frames the
extension performs with the `chrome.tabs`/`chrome.debugger` APIs:
opening, attaching, listing, and closing tabs.

The `chan` given to `connect` just needs the
[`ExtChannel`](https://AnswerDotAI.github.io/fastcdp/ext.html#extchannel)
surface (`request`, `events`, `close`, `is_open`), so other transports
plug in without subclassing ceremony: dialoghelper’s `Channel` is one,
which is how solvecdp drives the extension through a solveit relay.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/ext.py#L150"
target="_blank" style="float:right; font-size:smaller">source</a>

### ExtCDP.pages

``` python
async def pages():
```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/ext.py#L143"
target="_blank" style="float:right; font-size:smaller">source</a>

### ExtCDP.attach_page

``` python
async def attach_page(
    tid
):
```

*Attach to an existing tab and return a
[`Page`](https://AnswerDotAI.github.io/fastcdp/core.html#page) driving
it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/ext.py#L136"
target="_blank" style="float:right; font-size:smaller">source</a>

### ExtCDP.new_page

``` python
async def new_page(
    url:str='about:blank', active:bool=False
):
```

*Open a new browser tab and return a
[`Page`](https://AnswerDotAI.github.io/fastcdp/core.html#page) driving
it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/ext.py#L129"
target="_blank" style="float:right; font-size:smaller">source</a>

### ExtPage

``` python
def ExtPage(
    cdp, t, sid, owned:bool=False
):
```

*A [`Page`](https://AnswerDotAI.github.io/fastcdp/core.html#page) whose
tab is closed through the extension*

`new_page`, `attach_page`, and `pages` mirror fastcdp’s tab lifecycle
through extension actions, returning
[`Page`](https://AnswerDotAI.github.io/fastcdp/core.html#page) proxies
so `goto`, `eval`, `ax_tree`, and friends all work unchanged. Continuing
with the fake extension from above: a command sent with a `sid` reaches
the extension with that `tabId`:

``` python
cdp = await ExtCDP.connect(chan)
r = await cdp('Runtime.evaluate', sid=99, expression='1')
test_eq((r['method'], r['tabId']), ('Runtime.evaluate', 99))
```

Debugger events cross as `{method, params, tabId}` frames and land in
the inherited subscription machinery, with the `tabId` restored to a
`sessionId`:

``` python
async with cdp.on('Page.loadEventFired') as q:
    page = await cdp.new_page()
    e = await asyncio.wait_for(q.get(), 5)
test_eq((page.t, e['sessionId']), (99, 99))
```

`pages` lists the extension’s targets, and closing tears down the tab,
the pump, and the listener:

``` python
test_eq((await cdp.pages)[0]['type'], 'page')
await page.close()
await cdp.close()
await fake
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcdp/blob/main/fastcdp/core.py#L635"
target="_blank" style="float:right; font-size:smaller">source</a>

### cdp_yolo

``` python
def cdp_yolo():
```

*Allow all CDP classes in safepyrun, including the extension transport*
