summaryrefslogtreecommitdiff
path: root/crocoite/devtools.py
diff options
context:
space:
mode:
Diffstat (limited to 'crocoite/devtools.py')
-rw-r--r--crocoite/devtools.py102
1 files changed, 78 insertions, 24 deletions
diff --git a/crocoite/devtools.py b/crocoite/devtools.py
index b071d2e..8b5c69d 100644
--- a/crocoite/devtools.py
+++ b/crocoite/devtools.py
@@ -25,7 +25,12 @@ Communication with Google Chrome through its DevTools protocol.
import json, asyncio, logging, os
from tempfile import mkdtemp
import shutil
+from http.cookies import Morsel
+
import aiohttp, websockets
+from yarl import URL
+
+from .util import StrJsonEncoder
logger = logging.getLogger (__name__)
@@ -37,18 +42,17 @@ class Browser:
Destroyed upon exit.
"""
- __slots__ = ('session', 'url', 'tab', 'loop')
+ __slots__ = ('session', 'url', 'tab')
- def __init__ (self, url, loop=None):
- self.url = url
+ def __init__ (self, url):
+ self.url = URL (url)
self.session = None
self.tab = None
- self.loop = loop
async def __aiter__ (self):
""" List all tabs """
- async with aiohttp.ClientSession (loop=self.loop) as session:
- async with session.get ('{}/json/list'.format (self.url)) as r:
+ async with aiohttp.ClientSession () as session:
+ async with session.get (self.url.with_path ('/json/list')) as r:
resp = await r.json ()
for tab in resp:
if tab['type'] == 'page':
@@ -58,22 +62,35 @@ class Browser:
""" Create tab """
assert self.tab is None
assert self.session is None
- self.session = aiohttp.ClientSession (loop=self.loop)
- async with self.session.get ('{}/json/new'.format (self.url)) as r:
+ self.session = aiohttp.ClientSession ()
+ async with self.session.get (self.url.with_path ('/json/new')) as r:
resp = await r.json ()
self.tab = await Tab.create (**resp)
return self.tab
- async def __aexit__ (self, *args):
+ async def __aexit__ (self, excType, excValue, traceback):
assert self.tab is not None
assert self.session is not None
+
await self.tab.close ()
- async with self.session.get ('{}/json/close/{}'.format (self.url, self.tab.id)) as r:
- resp = await r.text ()
- assert resp == 'Target is closing'
+
+ try:
+ async with self.session.get (self.url.with_path (f'/json/close/{self.tab.id}')) as r:
+ resp = await r.text ()
+ assert resp == 'Target is closing'
+ except aiohttp.client_exceptions.ClientConnectorError:
+ # oh boy, the whole browser crashed instead
+ if excType is Crashed:
+ # exception is reraised by `return False`
+ pass
+ else:
+ # this one is more important
+ raise
+
self.tab = None
await self.session.close ()
self.session = None
+
return False
class TabFunction:
@@ -101,13 +118,13 @@ class TabFunction:
return hash (self.name)
def __getattr__ (self, k):
- return TabFunction ('{}.{}'.format (self.name, k), self.tab)
+ return TabFunction (f'{self.name}.{k}', self.tab)
async def __call__ (self, **kwargs):
return await self.tab (self.name, **kwargs)
def __repr__ (self):
- return '<TabFunction {}>'.format (self.name)
+ return f'<TabFunction {self.name}>'
class TabException (Exception):
pass
@@ -154,8 +171,8 @@ class Tab:
self.msgid += 1
message = {'method': method, 'params': kwargs, 'id': msgid}
t = self.transactions[msgid] = {'event': asyncio.Event (), 'result': None}
- logger.debug ('← {}'.format (message))
- await self.ws.send (json.dumps (message))
+ logger.debug (f'← {message}')
+ await self.ws.send (json.dumps (message, cls=StrJsonEncoder))
await t['event'].wait ()
ret = t['result']
del self.transactions[msgid]
@@ -189,7 +206,7 @@ class Tab:
# right now we cannot recover from this
await markCrashed (e)
break
- logger.debug ('→ {}'.format (msg))
+ logger.debug (f'→ {msg}')
if 'id' in msg:
msgid = msg['id']
t = self.transactions.get (msgid, None)
@@ -266,11 +283,11 @@ class Process:
async def __aenter__ (self):
assert self.p is None
- self.userDataDir = mkdtemp ()
+ self.userDataDir = mkdtemp (prefix=__package__ + '-chrome-userdata-')
# see https://github.com/GoogleChrome/chrome-launcher/blob/master/docs/chrome-flags-for-tools.md
args = [self.binary,
'--window-size={},{}'.format (*self.windowSize),
- '--user-data-dir={}'.format (self.userDataDir), # use temporory user dir
+ f'--user-data-dir={self.userDataDir}', # use temporory user dir
'--no-default-browser-check',
'--no-first-run', # don’t show first run screen
'--disable-breakpad', # no error reports
@@ -315,12 +332,26 @@ class Process:
if port is None:
raise Exception ('Chrome died on us.')
- return 'http://localhost:{}'.format (port)
+ return URL.build(scheme='http', host='localhost', port=port)
async def __aexit__ (self, *exc):
- self.p.terminate ()
- await self.p.wait ()
- shutil.rmtree (self.userDataDir)
+ try:
+ self.p.terminate ()
+ await self.p.wait ()
+ except ProcessLookupError:
+ # ok, fine, dead already
+ pass
+
+ # Try to delete the temporary directory multiple times. It looks like
+ # Chrome will change files in there even after it exited (i.e. .wait()
+ # returned). Very strange.
+ for i in range (5):
+ try:
+ shutil.rmtree (self.userDataDir)
+ break
+ except:
+ await asyncio.sleep (0.2)
+
self.p = None
return False
@@ -328,7 +359,7 @@ class Passthrough:
__slots__ = ('url', )
def __init__ (self, url):
- self.url = url
+ self.url = URL (url)
async def __aenter__ (self):
return self.url
@@ -336,3 +367,26 @@ class Passthrough:
async def __aexit__ (self, *exc):
return False
+def toCookieParam (m):
+ """
+ Convert Python’s http.cookies.Morsel to Chrome’s CookieParam, see
+ https://chromedevtools.github.io/devtools-protocol/1-3/Network#type-CookieParam
+ """
+
+ assert isinstance (m, Morsel)
+
+ out = {'name': m.key, 'value': m.value}
+
+ # unsupported by chrome
+ for k in ('max-age', 'comment', 'version'):
+ if m[k]:
+ raise ValueError (f'Unsupported cookie attribute {k} set, cannot convert')
+
+ for mname, cname in [('expires', None), ('path', None), ('domain', None), ('secure', None), ('httponly', 'httpOnly')]:
+ value = m[mname]
+ if value:
+ cname = cname or mname
+ out[cname] = value
+
+ return out
+