├── .gitignore ├── chrome_control ├── base.py ├── Inspector.py ├── Schema.py ├── Tethering.py ├── DeviceOrientation.py ├── Storage.py ├── IO.py ├── Memory.py ├── __init__.py ├── Console.py ├── Rendering.py ├── DOMStorage.py ├── Database.py ├── SystemInfo.py ├── CacheStorage.py ├── Log.py ├── ApplicationCache.py ├── Profiler.py ├── Tracing.py ├── chrome.py ├── Security.py ├── HeapProfiler.py ├── ServiceWorker.py ├── DOMDebugger.py ├── Target.py ├── IndexedDB.py ├── Animation.py ├── LayerTree.py ├── Accessibility.py ├── Emulation.py ├── Input.py ├── Debugger.py ├── Page.py ├── Runtime.py ├── Network.py ├── CSS.py └── DOM.py ├── setup.py ├── test.py └── gen.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | 4 | venv/ 5 | -------------------------------------------------------------------------------- /chrome_control/base.py: -------------------------------------------------------------------------------- 1 | class ChromeCommand: pass 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='chrome_control', 5 | version='0.1', 6 | packages=['chrome_control'], 7 | ) 8 | -------------------------------------------------------------------------------- /chrome_control/Inspector.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class enable(ChromeCommand): 8 | """Enables inspector domain notifications.""" 9 | 10 | def __init__(self): pass 11 | 12 | class disable(ChromeCommand): 13 | """Disables inspector domain notifications.""" 14 | 15 | def __init__(self): pass 16 | 17 | -------------------------------------------------------------------------------- /chrome_control/Schema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class Domain: 8 | """Description of the protocol domain.""" 9 | def __init__(self, name: str, version: str): 10 | # Domain name. 11 | self.name = name 12 | # Domain version. 13 | self.version = version 14 | 15 | class getDomains(ChromeCommand): 16 | """Returns supported domains.""" 17 | 18 | def __init__(self): pass 19 | 20 | -------------------------------------------------------------------------------- /chrome_control/Tethering.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class bind(ChromeCommand): 8 | """Request browser port binding.""" 9 | 10 | def __init__(self, port: int): 11 | # Port number to bind. 12 | self.port = port 13 | 14 | 15 | 16 | class unbind(ChromeCommand): 17 | """Request browser port unbinding.""" 18 | 19 | def __init__(self, port: int): 20 | # Port number to unbind. 21 | self.port = port 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from chrome_control import Chrome, Page, Runtime 4 | 5 | c = Chrome() 6 | c.do(Page.navigate("http://adhocteam.us/our-team")) 7 | 8 | # if we don't wait for the page to load, then we can run the script too 9 | # early and get an empty array. 10 | # 11 | # TODO a great next step would be to figure out how to receive pageLoad 12 | # event from Page and only run the command at that time 13 | time.sleep(2) 14 | 15 | cmd = '[].map.call(document.querySelectorAll("h3.centered"), n => n.textContent)' 16 | c.do(Runtime.evaluate(cmd, returnByValue=True)) 17 | -------------------------------------------------------------------------------- /chrome_control/DeviceOrientation.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class setDeviceOrientationOverride(ChromeCommand): 8 | """Overrides the Device Orientation.""" 9 | 10 | def __init__(self, alpha: float, beta: float, gamma: float): 11 | # Mock alpha 12 | self.alpha = alpha 13 | # Mock beta 14 | self.beta = beta 15 | # Mock gamma 16 | self.gamma = gamma 17 | 18 | 19 | 20 | class clearDeviceOrientationOverride(ChromeCommand): 21 | """Clears the overridden Device Orientation.""" 22 | 23 | def __init__(self): pass 24 | 25 | -------------------------------------------------------------------------------- /chrome_control/Storage.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | StorageType = Enum("StorageType", "appcache cookies file_systems indexeddb local_storage shader_cache websql service_workers cache_storage all") 8 | StorageType.__doc__ = """Enum of possible storage types.""" 9 | 10 | class clearDataForOrigin(ChromeCommand): 11 | """Clears storage for origin.""" 12 | 13 | def __init__(self, origin: str, storageTypes: str): 14 | # Security origin. 15 | self.origin = origin 16 | # Comma separated origin names. 17 | self.storageTypes = storageTypes 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /chrome_control/IO.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | StreamHandle = str 8 | 9 | class read(ChromeCommand): 10 | """Read a chunk of the stream""" 11 | 12 | def __init__(self, handle: "StreamHandle", offset: int=None, size: int=None): 13 | # Handle of the stream to read. 14 | self.handle = handle 15 | # Seek to the specified offset before reading (if not specificed, proceed with offset following the last read). 16 | self.offset = offset 17 | # Maximum number of bytes to read (left upon the agent discretion if not specified). 18 | self.size = size 19 | 20 | 21 | 22 | class close(ChromeCommand): 23 | """Close the stream, discard any temporary backing storage.""" 24 | 25 | def __init__(self, handle: "StreamHandle"): 26 | # Handle of the stream to close. 27 | self.handle = handle 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /chrome_control/Memory.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | PressureLevel = Enum("PressureLevel", "moderate critical") 8 | PressureLevel.__doc__ = """Memory pressure level.""" 9 | 10 | class getDOMCounters(ChromeCommand): 11 | def __init__(self): pass 12 | 13 | class setPressureNotificationsSuppressed(ChromeCommand): 14 | """Enable/disable suppressing memory pressure notifications in all processes.""" 15 | 16 | def __init__(self, suppressed: bool): 17 | # If true, memory pressure notifications will be suppressed. 18 | self.suppressed = suppressed 19 | 20 | 21 | 22 | class simulatePressureNotification(ChromeCommand): 23 | """Simulate a memory pressure notification in all processes.""" 24 | 25 | def __init__(self, level: "PressureLevel"): 26 | # Memory pressure level of the notification. 27 | self.level = level 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /chrome_control/__init__.py: -------------------------------------------------------------------------------- 1 | from .chrome import Chrome 2 | from . import Accessibility 3 | from . import Animation 4 | from . import ApplicationCache 5 | from . import CSS 6 | from . import CacheStorage 7 | from . import Console 8 | from . import DOM 9 | from . import DOMDebugger 10 | from . import DOMStorage 11 | from . import Database 12 | from . import Debugger 13 | from . import DeviceOrientation 14 | from . import Emulation 15 | from . import HeapProfiler 16 | from . import IO 17 | from . import IndexedDB 18 | from . import Input 19 | from . import Inspector 20 | from . import LayerTree 21 | from . import Log 22 | from . import Memory 23 | from . import Network 24 | from . import Page 25 | from . import Profiler 26 | from . import Rendering 27 | from . import Runtime 28 | from . import Schema 29 | from . import Security 30 | from . import ServiceWorker 31 | from . import Storage 32 | from . import SystemInfo 33 | from . import Target 34 | from . import Tethering 35 | -------------------------------------------------------------------------------- /chrome_control/Console.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class ConsoleMessage: 8 | """Console message.""" 9 | def __init__(self, source: str, level: str, text: str, url: str=None, line: int=None, column: int=None): 10 | # Message source. 11 | self.source = source 12 | # Message severity. 13 | self.level = level 14 | # Message text. 15 | self.text = text 16 | # URL of the message origin. 17 | self.url = url 18 | # Line number in the resource that generated this message (1-based). 19 | self.line = line 20 | # Column number in the resource that generated this message (1-based). 21 | self.column = column 22 | 23 | class enable(ChromeCommand): 24 | """Enables console domain, sends the messages collected so far to the client by means of the messageAdded notification.""" 25 | 26 | def __init__(self): pass 27 | 28 | class disable(ChromeCommand): 29 | """Disables console domain, prevents further console messages from being reported to the client.""" 30 | 31 | def __init__(self): pass 32 | 33 | class clearMessages(ChromeCommand): 34 | """Does nothing.""" 35 | 36 | def __init__(self): pass 37 | 38 | -------------------------------------------------------------------------------- /chrome_control/Rendering.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class setShowPaintRects(ChromeCommand): 8 | """Requests that backend shows paint rectangles""" 9 | 10 | def __init__(self, result: bool): 11 | # True for showing paint rectangles 12 | self.result = result 13 | 14 | 15 | 16 | class setShowDebugBorders(ChromeCommand): 17 | """Requests that backend shows debug borders on layers""" 18 | 19 | def __init__(self, show: bool): 20 | # True for showing debug borders 21 | self.show = show 22 | 23 | 24 | 25 | class setShowFPSCounter(ChromeCommand): 26 | """Requests that backend shows the FPS counter""" 27 | 28 | def __init__(self, show: bool): 29 | # True for showing the FPS counter 30 | self.show = show 31 | 32 | 33 | 34 | class setShowScrollBottleneckRects(ChromeCommand): 35 | """Requests that backend shows scroll bottleneck rects""" 36 | 37 | def __init__(self, show: bool): 38 | # True for showing scroll bottleneck rects 39 | self.show = show 40 | 41 | 42 | 43 | class setShowViewportSizeOnResize(ChromeCommand): 44 | """Paints viewport size upon main frame resize.""" 45 | 46 | def __init__(self, show: bool): 47 | # Whether to paint size or not. 48 | self.show = show 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /chrome_control/DOMStorage.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class StorageId: 8 | """DOM Storage identifier.""" 9 | def __init__(self, securityOrigin: str, isLocalStorage: bool): 10 | # Security origin for the storage. 11 | self.securityOrigin = securityOrigin 12 | # Whether the storage is local storage (not session storage). 13 | self.isLocalStorage = isLocalStorage 14 | 15 | # DOM Storage item. 16 | Item = List[str] 17 | class enable(ChromeCommand): 18 | """Enables storage tracking, storage events will now be delivered to the client.""" 19 | 20 | def __init__(self): pass 21 | 22 | class disable(ChromeCommand): 23 | """Disables storage tracking, prevents storage events from being sent to the client.""" 24 | 25 | def __init__(self): pass 26 | 27 | class getDOMStorageItems(ChromeCommand): 28 | def __init__(self, storageId: "StorageId"): 29 | self.storageId = storageId 30 | 31 | 32 | 33 | class setDOMStorageItem(ChromeCommand): 34 | def __init__(self, storageId: "StorageId", key: str, value: str): 35 | self.storageId = storageId 36 | self.key = key 37 | self.value = value 38 | 39 | 40 | 41 | class removeDOMStorageItem(ChromeCommand): 42 | def __init__(self, storageId: "StorageId", key: str): 43 | self.storageId = storageId 44 | self.key = key 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /chrome_control/Database.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | # Unique identifier of Database object. 8 | DatabaseId = str 9 | 10 | class Database: 11 | """Database object.""" 12 | def __init__(self, id: "DatabaseId", domain: str, name: str, version: str): 13 | # Database ID. 14 | self.id = id 15 | # Database domain. 16 | self.domain = domain 17 | # Database name. 18 | self.name = name 19 | # Database version. 20 | self.version = version 21 | 22 | class Error: 23 | """Database error.""" 24 | def __init__(self, message: str, code: int): 25 | # Error message. 26 | self.message = message 27 | # Error code. 28 | self.code = code 29 | 30 | class enable(ChromeCommand): 31 | """Enables database tracking, database events will now be delivered to the client.""" 32 | 33 | def __init__(self): pass 34 | 35 | class disable(ChromeCommand): 36 | """Disables database tracking, prevents database events from being sent to the client.""" 37 | 38 | def __init__(self): pass 39 | 40 | class getDatabaseTableNames(ChromeCommand): 41 | def __init__(self, databaseId: "DatabaseId"): 42 | self.databaseId = databaseId 43 | 44 | 45 | 46 | class executeSQL(ChromeCommand): 47 | def __init__(self, databaseId: "DatabaseId", query: str): 48 | self.databaseId = databaseId 49 | self.query = query 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /chrome_control/SystemInfo.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class GPUDevice: 8 | """Describes a single graphics processor (GPU).""" 9 | def __init__(self, vendorId: float, deviceId: float, vendorString: str, deviceString: str): 10 | # PCI ID of the GPU vendor, if available; 0 otherwise. 11 | self.vendorId = vendorId 12 | # PCI ID of the GPU device, if available; 0 otherwise. 13 | self.deviceId = deviceId 14 | # String description of the GPU vendor, if the PCI ID is not available. 15 | self.vendorString = vendorString 16 | # String description of the GPU device, if the PCI ID is not available. 17 | self.deviceString = deviceString 18 | 19 | class GPUInfo: 20 | """Provides information about the GPU(s) on the system.""" 21 | def __init__(self, devices: List, driverBugWorkarounds: List, auxAttributes: dict=None, featureStatus: dict=None): 22 | # The graphics devices on the system. Element 0 is the primary GPU. 23 | self.devices = devices 24 | # An optional array of GPU driver bug workarounds. 25 | self.driverBugWorkarounds = driverBugWorkarounds 26 | # An optional dictionary of additional GPU related attributes. 27 | self.auxAttributes = auxAttributes 28 | # An optional dictionary of graphics features and their status. 29 | self.featureStatus = featureStatus 30 | 31 | class getInfo(ChromeCommand): 32 | """Returns information about the system.""" 33 | 34 | def __init__(self): pass 35 | 36 | -------------------------------------------------------------------------------- /chrome_control/CacheStorage.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | # Unique identifier of the Cache object. 8 | CacheId = str 9 | 10 | class DataEntry: 11 | """Data entry.""" 12 | def __init__(self, request: str, response: str): 13 | # Request url spec. 14 | self.request = request 15 | # Response stataus text. 16 | self.response = response 17 | 18 | class Cache: 19 | """Cache identifier.""" 20 | def __init__(self, cacheId: "CacheId", securityOrigin: str, cacheName: str): 21 | # An opaque unique id of the cache. 22 | self.cacheId = cacheId 23 | # Security origin of the cache. 24 | self.securityOrigin = securityOrigin 25 | # The name of the cache. 26 | self.cacheName = cacheName 27 | 28 | class requestCacheNames(ChromeCommand): 29 | """Requests cache names.""" 30 | 31 | def __init__(self, securityOrigin: str): 32 | # Security origin. 33 | self.securityOrigin = securityOrigin 34 | 35 | 36 | 37 | class requestEntries(ChromeCommand): 38 | """Requests data from cache.""" 39 | 40 | def __init__(self, cacheId: "CacheId", skipCount: int, pageSize: int): 41 | # ID of cache to get entries from. 42 | self.cacheId = cacheId 43 | # Number of records to skip. 44 | self.skipCount = skipCount 45 | # Number of records to fetch. 46 | self.pageSize = pageSize 47 | 48 | 49 | 50 | class deleteCache(ChromeCommand): 51 | """Deletes a cache.""" 52 | 53 | def __init__(self, cacheId: "CacheId"): 54 | # Id of cache for deletion. 55 | self.cacheId = cacheId 56 | 57 | 58 | 59 | class deleteEntry(ChromeCommand): 60 | """Deletes a cache entry.""" 61 | 62 | def __init__(self, cacheId: "CacheId", request: str): 63 | # Id of cache where the entry will be deleted. 64 | self.cacheId = cacheId 65 | # URL spec of the request. 66 | self.request = request 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /chrome_control/Log.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Runtime 7 | from . import Network 8 | 9 | class LogEntry: 10 | """Log entry.""" 11 | def __init__(self, source: str, level: str, text: str, timestamp: "Runtime.Timestamp", url: str=None, lineNumber: int=None, stackTrace: "Runtime.StackTrace"=None, networkRequestId: "Network.RequestId"=None, workerId: str=None): 12 | # Log entry source. 13 | self.source = source 14 | # Log entry severity. 15 | self.level = level 16 | # Logged text. 17 | self.text = text 18 | # Timestamp when this entry was added. 19 | self.timestamp = timestamp 20 | # URL of the resource if known. 21 | self.url = url 22 | # Line number in the resource. 23 | self.lineNumber = lineNumber 24 | # JavaScript stack trace. 25 | self.stackTrace = stackTrace 26 | # Identifier of the network request associated with this entry. 27 | self.networkRequestId = networkRequestId 28 | # Identifier of the worker associated with this entry. 29 | self.workerId = workerId 30 | 31 | class ViolationSetting: 32 | """Violation configuration setting.""" 33 | def __init__(self, name: str, threshold: float): 34 | # Violation type. 35 | self.name = name 36 | # Time threshold to trigger upon. 37 | self.threshold = threshold 38 | 39 | class enable(ChromeCommand): 40 | """Enables log domain, sends the entries collected so far to the client by means of the entryAdded notification.""" 41 | 42 | def __init__(self): pass 43 | 44 | class disable(ChromeCommand): 45 | """Disables log domain, prevents further log entries from being reported to the client.""" 46 | 47 | def __init__(self): pass 48 | 49 | class clear(ChromeCommand): 50 | """Clears the log.""" 51 | 52 | def __init__(self): pass 53 | 54 | class startViolationsReport(ChromeCommand): 55 | """start violation reporting.""" 56 | 57 | def __init__(self, config: List): 58 | # Configuration for violations. 59 | self.config = config 60 | 61 | 62 | 63 | class stopViolationsReport(ChromeCommand): 64 | """Stop violation reporting.""" 65 | 66 | def __init__(self): pass 67 | 68 | -------------------------------------------------------------------------------- /chrome_control/ApplicationCache.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Page 7 | 8 | class ApplicationCacheResource: 9 | """Detailed application cache resource information.""" 10 | def __init__(self, url: str, size: int, type: str): 11 | # Resource url. 12 | self.url = url 13 | # Resource size. 14 | self.size = size 15 | # Resource type. 16 | self.type = type 17 | 18 | class ApplicationCache: 19 | """Detailed application cache information.""" 20 | def __init__(self, manifestURL: str, size: float, creationTime: float, updateTime: float, resources: List): 21 | # Manifest URL. 22 | self.manifestURL = manifestURL 23 | # Application cache size. 24 | self.size = size 25 | # Application cache creation time. 26 | self.creationTime = creationTime 27 | # Application cache update time. 28 | self.updateTime = updateTime 29 | # Application cache resources. 30 | self.resources = resources 31 | 32 | class FrameWithManifest: 33 | """Frame identifier - manifest URL pair.""" 34 | def __init__(self, frameId: "Page.FrameId", manifestURL: str, status: int): 35 | # Frame identifier. 36 | self.frameId = frameId 37 | # Manifest URL. 38 | self.manifestURL = manifestURL 39 | # Application cache status. 40 | self.status = status 41 | 42 | class getFramesWithManifests(ChromeCommand): 43 | """Returns array of frame identifiers with manifest urls for each frame containing a document associated with some application cache.""" 44 | 45 | def __init__(self): pass 46 | 47 | class enable(ChromeCommand): 48 | """Enables application cache domain notifications.""" 49 | 50 | def __init__(self): pass 51 | 52 | class getManifestForFrame(ChromeCommand): 53 | """Returns manifest URL for document in the given frame.""" 54 | 55 | def __init__(self, frameId: "Page.FrameId"): 56 | # Identifier of the frame containing document whose manifest is retrieved. 57 | self.frameId = frameId 58 | 59 | 60 | 61 | class getApplicationCacheForFrame(ChromeCommand): 62 | """Returns relevant application cache data for the document in given frame.""" 63 | 64 | def __init__(self, frameId: "Page.FrameId"): 65 | # Identifier of the frame containing document whose application cache is retrieved. 66 | self.frameId = frameId 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /chrome_control/Profiler.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Runtime 7 | 8 | class ProfileNode: 9 | """Profile node. Holds callsite information, execution statistics and child nodes.""" 10 | def __init__(self, id: int, callFrame: "Runtime.CallFrame", hitCount: int=None, children: List=None, deoptReason: str=None, positionTicks: List=None): 11 | # Unique id of the node. 12 | self.id = id 13 | # Function location. 14 | self.callFrame = callFrame 15 | # Number of samples where this node was on top of the call stack. 16 | self.hitCount = hitCount 17 | # Child node ids. 18 | self.children = children 19 | # The reason of being not optimized. The function may be deoptimized or marked as don't optimize. 20 | self.deoptReason = deoptReason 21 | # An array of source position ticks. 22 | self.positionTicks = positionTicks 23 | 24 | class Profile: 25 | """Profile.""" 26 | def __init__(self, nodes: List, startTime: float, endTime: float, samples: List=None, timeDeltas: List=None): 27 | # The list of profile nodes. First item is the root node. 28 | self.nodes = nodes 29 | # Profiling start timestamp in microseconds. 30 | self.startTime = startTime 31 | # Profiling end timestamp in microseconds. 32 | self.endTime = endTime 33 | # Ids of samples top nodes. 34 | self.samples = samples 35 | # Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime. 36 | self.timeDeltas = timeDeltas 37 | 38 | class PositionTickInfo: 39 | """Specifies a number of samples attributed to a certain source position.""" 40 | def __init__(self, line: int, ticks: int): 41 | # Source line number (1-based). 42 | self.line = line 43 | # Number of samples attributed to the source line. 44 | self.ticks = ticks 45 | 46 | class enable(ChromeCommand): 47 | def __init__(self): pass 48 | 49 | class disable(ChromeCommand): 50 | def __init__(self): pass 51 | 52 | class setSamplingInterval(ChromeCommand): 53 | """Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.""" 54 | 55 | def __init__(self, interval: int): 56 | # New sampling interval in microseconds. 57 | self.interval = interval 58 | 59 | 60 | 61 | class start(ChromeCommand): 62 | def __init__(self): pass 63 | 64 | class stop(ChromeCommand): 65 | def __init__(self): pass 66 | 67 | -------------------------------------------------------------------------------- /chrome_control/Tracing.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | class MemoryDumpConfig: pass 8 | 9 | class TraceConfig: 10 | def __init__(self, recordMode: str=None, enableSampling: bool=None, enableSystrace: bool=None, enableArgumentFilter: bool=None, includedCategories: List=None, excludedCategories: List=None, syntheticDelays: List=None, memoryDumpConfig: "MemoryDumpConfig"=None): 11 | # Controls how the trace buffer stores data. 12 | self.recordMode = recordMode 13 | # Turns on JavaScript stack sampling. 14 | self.enableSampling = enableSampling 15 | # Turns on system tracing. 16 | self.enableSystrace = enableSystrace 17 | # Turns on argument filter. 18 | self.enableArgumentFilter = enableArgumentFilter 19 | # Included category filters. 20 | self.includedCategories = includedCategories 21 | # Excluded category filters. 22 | self.excludedCategories = excludedCategories 23 | # Configuration to synthesize the delays in tracing. 24 | self.syntheticDelays = syntheticDelays 25 | # Configuration for memory dump triggers. Used only when "memory-infra" category is enabled. 26 | self.memoryDumpConfig = memoryDumpConfig 27 | 28 | class start(ChromeCommand): 29 | """Start trace events collection.""" 30 | 31 | def __init__(self, categories: str=None, options: str=None, bufferUsageReportingInterval: float=None, transferMode: str=None, traceConfig: "TraceConfig"=None): 32 | # Category/tag filter 33 | self.categories = categories 34 | # Tracing options 35 | self.options = options 36 | # If set, the agent will issue bufferUsage events at this interval, specified in milliseconds 37 | self.bufferUsageReportingInterval = bufferUsageReportingInterval 38 | # Whether to report trace events as series of dataCollected events or to save trace to a stream (defaults to ReportEvents). 39 | self.transferMode = transferMode 40 | # 41 | self.traceConfig = traceConfig 42 | 43 | 44 | 45 | class end(ChromeCommand): 46 | """Stop trace events collection.""" 47 | 48 | def __init__(self): pass 49 | 50 | class getCategories(ChromeCommand): 51 | """Gets supported tracing categories.""" 52 | 53 | def __init__(self): pass 54 | 55 | class requestMemoryDump(ChromeCommand): 56 | """Request a global memory dump.""" 57 | 58 | def __init__(self): pass 59 | 60 | class recordClockSyncMarker(ChromeCommand): 61 | """Record a clock sync marker in the trace.""" 62 | 63 | def __init__(self, syncId: str): 64 | # The ID of this clock sync marker 65 | self.syncId = syncId 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /chrome_control/chrome.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import websocket 4 | from chrome_control.base import ChromeCommand 5 | 6 | class ObjectEncoder(json.JSONEncoder): 7 | def default(self, obj): 8 | if isinstance(obj, (str, int, float, list, bool, dict)): 9 | return json.JSONEncoder.default(self, obj) 10 | attrs = [x for x in dir(obj) if not x.startswith('_')] 11 | return {key: getattr(obj, key) for key in attrs if getattr(obj, key) is not None} 12 | 13 | class Chrome: 14 | def __init__(self, debug=1): 15 | self.debug = debug 16 | 17 | # is this documented anywhere? I had to find this from reading node code 18 | # https://github.com/cyrus-and/chrome-remote-interface/blob/2a85a87574053f4ef48d17ac1ac03b7513310336/lib/devtools.js#L65 19 | # 20 | # there appears to be an endpoint that's equivalent to /json/new, at: 21 | # https://chromedevtools.github.io/debugger-protocol-viewer/tot/Target/#method-createTarget 22 | # 23 | # update, no this is not documented anywhere: 24 | # https://github.com/GoogleChrome/devtools-docs/issues/67#issuecomment-37675331 25 | self.tab = requests.get("http://localhost:9222/json/new").json() 26 | tabs = requests.get("http://localhost:9222/json").json() 27 | 28 | # we need to find the index of our new tab in the tablist, which the API 29 | # calls the "id", but also doesn't come back in the /new obj. Search the tablist 30 | # for our tab. 31 | for i, t in enumerate(tabs): 32 | if t["id"] == self.tab["id"]: 33 | self.idx = i 34 | break 35 | else: 1/0 36 | 37 | # * Maybe a tab is the proper unit for the client to deal with? Something like: 38 | # with Tab(url) as tab: 39 | # tab.do(Page.navigate("http://bananas.com")) 40 | # tab.do(Runtime.evaluate("console.log(window.location);", returnByValue=true)) 41 | self.ws = websocket.create_connection(self.tab['webSocketDebuggerUrl']) 42 | 43 | def do(self, cmd: ChromeCommand): 44 | # Reverse engineer the command name that was passed in from the object's 45 | # meta information. Converts class like `chrome_control.Page.navigate` 46 | # to `'Page.navigate'`. 47 | method = f'{cmd.__module__.split(".")[-1]}.{cmd.__class__.__name__}' 48 | 49 | msg = { 50 | "id": self.idx, 51 | "method": method, 52 | "params": cmd 53 | } 54 | if self.debug: 55 | print("sent: ", json.dumps(msg, cls=ObjectEncoder)) 56 | 57 | self.ws.send(json.dumps(msg, cls=ObjectEncoder)) 58 | 59 | o = json.loads(self.ws.recv()) 60 | if self.debug: 61 | print("rcvd: ", o) 62 | 63 | return o 64 | -------------------------------------------------------------------------------- /chrome_control/Security.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | # An internal certificate ID value. 8 | CertificateId = int 9 | 10 | SecurityState = Enum("SecurityState", "unknown neutral insecure warning secure info") 11 | SecurityState.__doc__ = """The security level of a page or resource.""" 12 | 13 | class SecurityStateExplanation: 14 | """An explanation of an factor contributing to the security state.""" 15 | def __init__(self, securityState: "SecurityState", summary: str, description: str, hasCertificate: bool): 16 | # Security state representing the severity of the factor being explained. 17 | self.securityState = securityState 18 | # Short phrase describing the type of factor. 19 | self.summary = summary 20 | # Full text explanation of the factor. 21 | self.description = description 22 | # True if the page has a certificate. 23 | self.hasCertificate = hasCertificate 24 | 25 | class InsecureContentStatus: 26 | """Information about insecure content on the page.""" 27 | def __init__(self, ranMixedContent: bool, displayedMixedContent: bool, ranContentWithCertErrors: bool, displayedContentWithCertErrors: bool, ranInsecureContentStyle: "SecurityState", displayedInsecureContentStyle: "SecurityState"): 28 | # True if the page was loaded over HTTPS and ran mixed (HTTP) content such as scripts. 29 | self.ranMixedContent = ranMixedContent 30 | # True if the page was loaded over HTTPS and displayed mixed (HTTP) content such as images. 31 | self.displayedMixedContent = displayedMixedContent 32 | # True if the page was loaded over HTTPS without certificate errors, and ran content such as scripts that were loaded with certificate errors. 33 | self.ranContentWithCertErrors = ranContentWithCertErrors 34 | # True if the page was loaded over HTTPS without certificate errors, and displayed content such as images that were loaded with certificate errors. 35 | self.displayedContentWithCertErrors = displayedContentWithCertErrors 36 | # Security state representing a page that ran insecure content. 37 | self.ranInsecureContentStyle = ranInsecureContentStyle 38 | # Security state representing a page that displayed insecure content. 39 | self.displayedInsecureContentStyle = displayedInsecureContentStyle 40 | 41 | class enable(ChromeCommand): 42 | """Enables tracking security state changes.""" 43 | 44 | def __init__(self): pass 45 | 46 | class disable(ChromeCommand): 47 | """Disables tracking security state changes.""" 48 | 49 | def __init__(self): pass 50 | 51 | class showCertificateViewer(ChromeCommand): 52 | """Displays native dialog with the certificate details.""" 53 | 54 | def __init__(self): pass 55 | 56 | -------------------------------------------------------------------------------- /chrome_control/HeapProfiler.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Runtime 7 | 8 | # Heap snapshot object id. 9 | HeapSnapshotObjectId = str 10 | 11 | class SamplingHeapProfileNode: 12 | """Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.""" 13 | def __init__(self, callFrame: "Runtime.CallFrame", selfSize: float, children: List): 14 | # Function location. 15 | self.callFrame = callFrame 16 | # Allocations size in bytes for the node excluding children. 17 | self.selfSize = selfSize 18 | # Child nodes. 19 | self.children = children 20 | 21 | class SamplingHeapProfile: 22 | """Profile.""" 23 | def __init__(self, head: "SamplingHeapProfileNode"): 24 | self.head = head 25 | 26 | class enable(ChromeCommand): 27 | def __init__(self): pass 28 | 29 | class disable(ChromeCommand): 30 | def __init__(self): pass 31 | 32 | class startTrackingHeapObjects(ChromeCommand): 33 | def __init__(self, trackAllocations: bool=None): 34 | self.trackAllocations = trackAllocations 35 | 36 | 37 | 38 | class stopTrackingHeapObjects(ChromeCommand): 39 | def __init__(self, reportProgress: bool=None): 40 | # If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken when the tracking is stopped. 41 | self.reportProgress = reportProgress 42 | 43 | 44 | 45 | class takeHeapSnapshot(ChromeCommand): 46 | def __init__(self, reportProgress: bool=None): 47 | # If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken. 48 | self.reportProgress = reportProgress 49 | 50 | 51 | 52 | class collectGarbage(ChromeCommand): 53 | def __init__(self): pass 54 | 55 | class getObjectByHeapObjectId(ChromeCommand): 56 | def __init__(self, objectId: "HeapSnapshotObjectId", objectGroup: str=None): 57 | self.objectId = objectId 58 | # Symbolic group name that can be used to release multiple objects. 59 | self.objectGroup = objectGroup 60 | 61 | 62 | 63 | class addInspectedHeapObject(ChromeCommand): 64 | """Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions).""" 65 | 66 | def __init__(self, heapObjectId: "HeapSnapshotObjectId"): 67 | # Heap snapshot object id to be accessible by means of $x command line API. 68 | self.heapObjectId = heapObjectId 69 | 70 | 71 | 72 | class getHeapObjectId(ChromeCommand): 73 | def __init__(self, objectId: "Runtime.RemoteObjectId"): 74 | # Identifier of the object to get heap object id for. 75 | self.objectId = objectId 76 | 77 | 78 | 79 | class startSampling(ChromeCommand): 80 | def __init__(self, samplingInterval: float=None): 81 | # Average sample interval in bytes. Poisson distribution is used for the intervals. The default value is 32768 bytes. 82 | self.samplingInterval = samplingInterval 83 | 84 | 85 | 86 | class stopSampling(ChromeCommand): 87 | def __init__(self): pass 88 | 89 | -------------------------------------------------------------------------------- /chrome_control/ServiceWorker.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Target 7 | 8 | class ServiceWorkerRegistration: 9 | """ServiceWorker registration.""" 10 | def __init__(self, registrationId: str, scopeURL: str, isDeleted: bool): 11 | self.registrationId = registrationId 12 | self.scopeURL = scopeURL 13 | self.isDeleted = isDeleted 14 | 15 | ServiceWorkerVersionRunningStatus = Enum("ServiceWorkerVersionRunningStatus", "stopped starting running stopping") 16 | ServiceWorkerVersionRunningStatus.__doc__ = """""" 17 | 18 | ServiceWorkerVersionStatus = Enum("ServiceWorkerVersionStatus", "new installing installed activating activated redundant") 19 | ServiceWorkerVersionStatus.__doc__ = """""" 20 | 21 | class ServiceWorkerVersion: 22 | """ServiceWorker version.""" 23 | def __init__(self, versionId: str, registrationId: str, scriptURL: str, runningStatus: "ServiceWorkerVersionRunningStatus", status: "ServiceWorkerVersionStatus", scriptLastModified: float=None, scriptResponseTime: float=None, controlledClients: List=None, targetId: "Target.TargetID"=None): 24 | self.versionId = versionId 25 | self.registrationId = registrationId 26 | self.scriptURL = scriptURL 27 | self.runningStatus = runningStatus 28 | self.status = status 29 | # The Last-Modified header value of the main script. 30 | self.scriptLastModified = scriptLastModified 31 | # The time at which the response headers of the main script were received from the server. For cached script it is the last time the cache entry was validated. 32 | self.scriptResponseTime = scriptResponseTime 33 | self.controlledClients = controlledClients 34 | self.targetId = targetId 35 | 36 | class ServiceWorkerErrorMessage: 37 | """ServiceWorker error message.""" 38 | def __init__(self, errorMessage: str, registrationId: str, versionId: str, sourceURL: str, lineNumber: int, columnNumber: int): 39 | self.errorMessage = errorMessage 40 | self.registrationId = registrationId 41 | self.versionId = versionId 42 | self.sourceURL = sourceURL 43 | self.lineNumber = lineNumber 44 | self.columnNumber = columnNumber 45 | 46 | class enable(ChromeCommand): 47 | def __init__(self): pass 48 | 49 | class disable(ChromeCommand): 50 | def __init__(self): pass 51 | 52 | class unregister(ChromeCommand): 53 | def __init__(self, scopeURL: str): 54 | self.scopeURL = scopeURL 55 | 56 | 57 | 58 | class updateRegistration(ChromeCommand): 59 | def __init__(self, scopeURL: str): 60 | self.scopeURL = scopeURL 61 | 62 | 63 | 64 | class startWorker(ChromeCommand): 65 | def __init__(self, scopeURL: str): 66 | self.scopeURL = scopeURL 67 | 68 | 69 | 70 | class skipWaiting(ChromeCommand): 71 | def __init__(self, scopeURL: str): 72 | self.scopeURL = scopeURL 73 | 74 | 75 | 76 | class stopWorker(ChromeCommand): 77 | def __init__(self, versionId: str): 78 | self.versionId = versionId 79 | 80 | 81 | 82 | class inspectWorker(ChromeCommand): 83 | def __init__(self, versionId: str): 84 | self.versionId = versionId 85 | 86 | 87 | 88 | class setForceUpdateOnPageLoad(ChromeCommand): 89 | def __init__(self, forceUpdateOnPageLoad: bool): 90 | self.forceUpdateOnPageLoad = forceUpdateOnPageLoad 91 | 92 | 93 | 94 | class deliverPushMessage(ChromeCommand): 95 | def __init__(self, origin: str, registrationId: str, data: str): 96 | self.origin = origin 97 | self.registrationId = registrationId 98 | self.data = data 99 | 100 | 101 | 102 | class dispatchSyncEvent(ChromeCommand): 103 | def __init__(self, origin: str, registrationId: str, tag: str, lastChance: bool): 104 | self.origin = origin 105 | self.registrationId = registrationId 106 | self.tag = tag 107 | self.lastChance = lastChance 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /chrome_control/DOMDebugger.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Runtime 7 | 8 | DOMBreakpointType = Enum("DOMBreakpointType", "subtree-modified attribute-modified node-removed") 9 | DOMBreakpointType.__doc__ = """DOM breakpoint type.""" 10 | 11 | class EventListener: 12 | """Object event listener.""" 13 | def __init__(self, type: str, useCapture: bool, passive: bool, once: bool, scriptId: "Runtime.ScriptId", lineNumber: int, columnNumber: int, handler: "Runtime.RemoteObject"=None, originalHandler: "Runtime.RemoteObject"=None, removeFunction: "Runtime.RemoteObject"=None): 14 | # EventListener's type. 15 | self.type = type 16 | # EventListener's useCapture. 17 | self.useCapture = useCapture 18 | # EventListener's passive flag. 19 | self.passive = passive 20 | # EventListener's once flag. 21 | self.once = once 22 | # Script id of the handler code. 23 | self.scriptId = scriptId 24 | # Line number in the script (0-based). 25 | self.lineNumber = lineNumber 26 | # Column number in the script (0-based). 27 | self.columnNumber = columnNumber 28 | # Event handler function value. 29 | self.handler = handler 30 | # Event original handler function value. 31 | self.originalHandler = originalHandler 32 | # Event listener remove function. 33 | self.removeFunction = removeFunction 34 | 35 | class setDOMBreakpoint(ChromeCommand): 36 | """Sets breakpoint on particular operation with DOM.""" 37 | 38 | def __init__(self, nodeId: "DOM.NodeId", type: "DOMBreakpointType"): 39 | # Identifier of the node to set breakpoint on. 40 | self.nodeId = nodeId 41 | # Type of the operation to stop upon. 42 | self.type = type 43 | 44 | 45 | 46 | class removeDOMBreakpoint(ChromeCommand): 47 | """Removes DOM breakpoint that was set using setDOMBreakpoint.""" 48 | 49 | def __init__(self, nodeId: "DOM.NodeId", type: "DOMBreakpointType"): 50 | # Identifier of the node to remove breakpoint from. 51 | self.nodeId = nodeId 52 | # Type of the breakpoint to remove. 53 | self.type = type 54 | 55 | 56 | 57 | class setEventListenerBreakpoint(ChromeCommand): 58 | """Sets breakpoint on particular DOM event.""" 59 | 60 | def __init__(self, eventName: str, targetName: str=None): 61 | # DOM Event name to stop on (any DOM event will do). 62 | self.eventName = eventName 63 | # EventTarget interface name to stop on. If equal to "*" or not provided, will stop on any EventTarget. 64 | self.targetName = targetName 65 | 66 | 67 | 68 | class removeEventListenerBreakpoint(ChromeCommand): 69 | """Removes breakpoint on particular DOM event.""" 70 | 71 | def __init__(self, eventName: str, targetName: str=None): 72 | # Event name. 73 | self.eventName = eventName 74 | # EventTarget interface name. 75 | self.targetName = targetName 76 | 77 | 78 | 79 | class setInstrumentationBreakpoint(ChromeCommand): 80 | """Sets breakpoint on particular native event.""" 81 | 82 | def __init__(self, eventName: str): 83 | # Instrumentation name to stop on. 84 | self.eventName = eventName 85 | 86 | 87 | 88 | class removeInstrumentationBreakpoint(ChromeCommand): 89 | """Removes breakpoint on particular native event.""" 90 | 91 | def __init__(self, eventName: str): 92 | # Instrumentation name to stop on. 93 | self.eventName = eventName 94 | 95 | 96 | 97 | class setXHRBreakpoint(ChromeCommand): 98 | """Sets breakpoint on XMLHttpRequest.""" 99 | 100 | def __init__(self, url: str): 101 | # Resource URL substring. All XHRs having this substring in the URL will get stopped upon. 102 | self.url = url 103 | 104 | 105 | 106 | class removeXHRBreakpoint(ChromeCommand): 107 | """Removes breakpoint from XMLHttpRequest.""" 108 | 109 | def __init__(self, url: str): 110 | # Resource URL substring. 111 | self.url = url 112 | 113 | 114 | 115 | class getEventListeners(ChromeCommand): 116 | """Returns event listeners of the given object.""" 117 | 118 | def __init__(self, objectId: "Runtime.RemoteObjectId"): 119 | # Identifier of the object to return listeners for. 120 | self.objectId = objectId 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /chrome_control/Target.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | 7 | TargetID = str 8 | 9 | BrowserContextID = str 10 | 11 | class TargetInfo: 12 | def __init__(self, targetId: "TargetID", type: str, title: str, url: str): 13 | self.targetId = targetId 14 | self.type = type 15 | self.title = title 16 | self.url = url 17 | 18 | class RemoteLocation: 19 | def __init__(self, host: str, port: int): 20 | self.host = host 21 | self.port = port 22 | 23 | class setDiscoverTargets(ChromeCommand): 24 | """Controls whether to discover available targets and notify via targetCreated/targetDestroyed events.""" 25 | 26 | def __init__(self, discover: bool): 27 | # Whether to discover available targets. 28 | self.discover = discover 29 | 30 | 31 | 32 | class setAutoAttach(ChromeCommand): 33 | """Controls whether to automatically attach to new targets which are considered to be related to this one. When turned on, attaches to all existing related targets as well. When turned off, automatically detaches from all currently attached targets.""" 34 | 35 | def __init__(self, autoAttach: bool, waitForDebuggerOnStart: bool): 36 | # Whether to auto-attach to related targets. 37 | self.autoAttach = autoAttach 38 | # Whether to pause new targets when attaching to them. Use Runtime.runIfWaitingForDebugger to run paused targets. 39 | self.waitForDebuggerOnStart = waitForDebuggerOnStart 40 | 41 | 42 | 43 | class setAttachToFrames(ChromeCommand): 44 | def __init__(self, value: bool): 45 | # Whether to attach to frames. 46 | self.value = value 47 | 48 | 49 | 50 | class setRemoteLocations(ChromeCommand): 51 | """Enables target discovery for the specified locations, when setDiscoverTargets was set to true.""" 52 | 53 | def __init__(self, locations: List): 54 | # List of remote locations. 55 | self.locations = locations 56 | 57 | 58 | 59 | class sendMessageToTarget(ChromeCommand): 60 | """Sends protocol message to the target with given id.""" 61 | 62 | def __init__(self, targetId: str, message: str): 63 | self.targetId = targetId 64 | self.message = message 65 | 66 | 67 | 68 | class getTargetInfo(ChromeCommand): 69 | """Returns information about a target.""" 70 | 71 | def __init__(self, targetId: "TargetID"): 72 | self.targetId = targetId 73 | 74 | 75 | 76 | class activateTarget(ChromeCommand): 77 | """Activates (focuses) the target.""" 78 | 79 | def __init__(self, targetId: "TargetID"): 80 | self.targetId = targetId 81 | 82 | 83 | 84 | class closeTarget(ChromeCommand): 85 | """Closes the target. If the target is a page that gets closed too.""" 86 | 87 | def __init__(self, targetId: "TargetID"): 88 | self.targetId = targetId 89 | 90 | 91 | 92 | class attachToTarget(ChromeCommand): 93 | """Attaches to the target with given id.""" 94 | 95 | def __init__(self, targetId: "TargetID"): 96 | self.targetId = targetId 97 | 98 | 99 | 100 | class detachFromTarget(ChromeCommand): 101 | """Detaches from the target with given id.""" 102 | 103 | def __init__(self, targetId: "TargetID"): 104 | self.targetId = targetId 105 | 106 | 107 | 108 | class createBrowserContext(ChromeCommand): 109 | """Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than one.""" 110 | 111 | def __init__(self): pass 112 | 113 | class disposeBrowserContext(ChromeCommand): 114 | """Deletes a BrowserContext, will fail of any open page uses it.""" 115 | 116 | def __init__(self, browserContextId: "BrowserContextID"): 117 | self.browserContextId = browserContextId 118 | 119 | 120 | 121 | class createTarget(ChromeCommand): 122 | """Creates a new page.""" 123 | 124 | def __init__(self, url: str, width: int=None, height: int=None, browserContextId: "BrowserContextID"=None): 125 | # The initial URL the page will be navigated to. 126 | self.url = url 127 | # Frame width in DIP (headless chrome only). 128 | self.width = width 129 | # Frame height in DIP (headless chrome only). 130 | self.height = height 131 | # The browser context to create the page in (headless chrome only). 132 | self.browserContextId = browserContextId 133 | 134 | 135 | 136 | class getTargets(ChromeCommand): 137 | """Retrieves a list of available targets.""" 138 | 139 | def __init__(self): pass 140 | 141 | -------------------------------------------------------------------------------- /chrome_control/IndexedDB.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import Runtime 7 | 8 | class DatabaseWithObjectStores: 9 | """Database with an array of object stores.""" 10 | def __init__(self, name: str, version: int, objectStores: List): 11 | # Database name. 12 | self.name = name 13 | # Database version. 14 | self.version = version 15 | # Object stores in this database. 16 | self.objectStores = objectStores 17 | 18 | class ObjectStore: 19 | """Object store.""" 20 | def __init__(self, name: str, keyPath: "KeyPath", autoIncrement: bool, indexes: List): 21 | # Object store name. 22 | self.name = name 23 | # Object store key path. 24 | self.keyPath = keyPath 25 | # If true, object store has auto increment flag set. 26 | self.autoIncrement = autoIncrement 27 | # Indexes in this object store. 28 | self.indexes = indexes 29 | 30 | class ObjectStoreIndex: 31 | """Object store index.""" 32 | def __init__(self, name: str, keyPath: "KeyPath", unique: bool, multiEntry: bool): 33 | # Index name. 34 | self.name = name 35 | # Index key path. 36 | self.keyPath = keyPath 37 | # If true, index is unique. 38 | self.unique = unique 39 | # If true, index allows multiple entries for a key. 40 | self.multiEntry = multiEntry 41 | 42 | class Key: 43 | """Key.""" 44 | def __init__(self, type: str, number: float=None, string: str=None, date: float=None, array: List=None): 45 | # Key type. 46 | self.type = type 47 | # Number value. 48 | self.number = number 49 | # String value. 50 | self.string = string 51 | # Date value. 52 | self.date = date 53 | # Array value. 54 | self.array = array 55 | 56 | class KeyRange: 57 | """Key range.""" 58 | def __init__(self, lowerOpen: bool, upperOpen: bool, lower: "Key"=None, upper: "Key"=None): 59 | # If true lower bound is open. 60 | self.lowerOpen = lowerOpen 61 | # If true upper bound is open. 62 | self.upperOpen = upperOpen 63 | # Lower bound. 64 | self.lower = lower 65 | # Upper bound. 66 | self.upper = upper 67 | 68 | class DataEntry: 69 | """Data entry.""" 70 | def __init__(self, key: "Runtime.RemoteObject", primaryKey: "Runtime.RemoteObject", value: "Runtime.RemoteObject"): 71 | # Key object. 72 | self.key = key 73 | # Primary key object. 74 | self.primaryKey = primaryKey 75 | # Value object. 76 | self.value = value 77 | 78 | class KeyPath: 79 | """Key path.""" 80 | def __init__(self, type: str, string: str=None, array: List=None): 81 | # Key path type. 82 | self.type = type 83 | # String value. 84 | self.string = string 85 | # Array value. 86 | self.array = array 87 | 88 | class enable(ChromeCommand): 89 | """Enables events from backend.""" 90 | 91 | def __init__(self): pass 92 | 93 | class disable(ChromeCommand): 94 | """Disables events from backend.""" 95 | 96 | def __init__(self): pass 97 | 98 | class requestDatabaseNames(ChromeCommand): 99 | """Requests database names for given security origin.""" 100 | 101 | def __init__(self, securityOrigin: str): 102 | # Security origin. 103 | self.securityOrigin = securityOrigin 104 | 105 | 106 | 107 | class requestDatabase(ChromeCommand): 108 | """Requests database with given name in given frame.""" 109 | 110 | def __init__(self, securityOrigin: str, databaseName: str): 111 | # Security origin. 112 | self.securityOrigin = securityOrigin 113 | # Database name. 114 | self.databaseName = databaseName 115 | 116 | 117 | 118 | class requestData(ChromeCommand): 119 | """Requests data from object store or index.""" 120 | 121 | def __init__(self, securityOrigin: str, databaseName: str, objectStoreName: str, indexName: str, skipCount: int, pageSize: int, keyRange: "KeyRange"=None): 122 | # Security origin. 123 | self.securityOrigin = securityOrigin 124 | # Database name. 125 | self.databaseName = databaseName 126 | # Object store name. 127 | self.objectStoreName = objectStoreName 128 | # Index name, empty string for object store data requests. 129 | self.indexName = indexName 130 | # Number of records to skip. 131 | self.skipCount = skipCount 132 | # Number of records to fetch. 133 | self.pageSize = pageSize 134 | # Key range. 135 | self.keyRange = keyRange 136 | 137 | 138 | 139 | class clearObjectStore(ChromeCommand): 140 | """Clears all entries from an object store.""" 141 | 142 | def __init__(self, securityOrigin: str, databaseName: str, objectStoreName: str): 143 | # Security origin. 144 | self.securityOrigin = securityOrigin 145 | # Database name. 146 | self.databaseName = databaseName 147 | # Object store name. 148 | self.objectStoreName = objectStoreName 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /chrome_control/Animation.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import DOM 7 | 8 | class Animation: 9 | """Animation instance.""" 10 | def __init__(self, id: str, name: str, pausedState: bool, playState: str, playbackRate: float, startTime: float, currentTime: float, source: "AnimationEffect", type: str, cssId: str=None): 11 | # Animation's id. 12 | self.id = id 13 | # Animation's name. 14 | self.name = name 15 | # Animation's internal paused state. 16 | self.pausedState = pausedState 17 | # Animation's play state. 18 | self.playState = playState 19 | # Animation's playback rate. 20 | self.playbackRate = playbackRate 21 | # Animation's start time. 22 | self.startTime = startTime 23 | # Animation's current time. 24 | self.currentTime = currentTime 25 | # Animation's source animation node. 26 | self.source = source 27 | # Animation type of Animation. 28 | self.type = type 29 | # A unique ID for Animation representing the sources that triggered this CSS animation/transition. 30 | self.cssId = cssId 31 | 32 | class AnimationEffect: 33 | """AnimationEffect instance""" 34 | def __init__(self, delay: float, endDelay: float, iterationStart: float, iterations: float, duration: float, direction: str, fill: str, backendNodeId: "DOM.BackendNodeId", easing: str, keyframesRule: "KeyframesRule"=None): 35 | # AnimationEffect's delay. 36 | self.delay = delay 37 | # AnimationEffect's end delay. 38 | self.endDelay = endDelay 39 | # AnimationEffect's iteration start. 40 | self.iterationStart = iterationStart 41 | # AnimationEffect's iterations. 42 | self.iterations = iterations 43 | # AnimationEffect's iteration duration. 44 | self.duration = duration 45 | # AnimationEffect's playback direction. 46 | self.direction = direction 47 | # AnimationEffect's fill mode. 48 | self.fill = fill 49 | # AnimationEffect's target node. 50 | self.backendNodeId = backendNodeId 51 | # AnimationEffect's timing function. 52 | self.easing = easing 53 | # AnimationEffect's keyframes. 54 | self.keyframesRule = keyframesRule 55 | 56 | class KeyframesRule: 57 | """Keyframes Rule""" 58 | def __init__(self, keyframes: List, name: str=None): 59 | # List of animation keyframes. 60 | self.keyframes = keyframes 61 | # CSS keyframed animation's name. 62 | self.name = name 63 | 64 | class KeyframeStyle: 65 | """Keyframe Style""" 66 | def __init__(self, offset: str, easing: str): 67 | # Keyframe's time offset. 68 | self.offset = offset 69 | # AnimationEffect's timing function. 70 | self.easing = easing 71 | 72 | class enable(ChromeCommand): 73 | """Enables animation domain notifications.""" 74 | 75 | def __init__(self): pass 76 | 77 | class disable(ChromeCommand): 78 | """Disables animation domain notifications.""" 79 | 80 | def __init__(self): pass 81 | 82 | class getPlaybackRate(ChromeCommand): 83 | """Gets the playback rate of the document timeline.""" 84 | 85 | def __init__(self): pass 86 | 87 | class setPlaybackRate(ChromeCommand): 88 | """Sets the playback rate of the document timeline.""" 89 | 90 | def __init__(self, playbackRate: float): 91 | # Playback rate for animations on page 92 | self.playbackRate = playbackRate 93 | 94 | 95 | 96 | class getCurrentTime(ChromeCommand): 97 | """Returns the current time of the an animation.""" 98 | 99 | def __init__(self, id: str): 100 | # Id of animation. 101 | self.id = id 102 | 103 | 104 | 105 | class setPaused(ChromeCommand): 106 | """Sets the paused state of a set of animations.""" 107 | 108 | def __init__(self, animations: List, paused: bool): 109 | # Animations to set the pause state of. 110 | self.animations = animations 111 | # Paused state to set to. 112 | self.paused = paused 113 | 114 | 115 | 116 | class setTiming(ChromeCommand): 117 | """Sets the timing of an animation node.""" 118 | 119 | def __init__(self, animationId: str, duration: float, delay: float): 120 | # Animation id. 121 | self.animationId = animationId 122 | # Duration of the animation. 123 | self.duration = duration 124 | # Delay of the animation. 125 | self.delay = delay 126 | 127 | 128 | 129 | class seekAnimations(ChromeCommand): 130 | """Seek a set of animations to a particular time within each animation.""" 131 | 132 | def __init__(self, animations: List, currentTime: float): 133 | # List of animation ids to seek. 134 | self.animations = animations 135 | # Set the current time of each animation. 136 | self.currentTime = currentTime 137 | 138 | 139 | 140 | class releaseAnimations(ChromeCommand): 141 | """Releases a set of animations to no longer be manipulated.""" 142 | 143 | def __init__(self, animations: List): 144 | # List of animation ids to seek. 145 | self.animations = animations 146 | 147 | 148 | 149 | class resolveAnimation(ChromeCommand): 150 | """Gets the remote object of the Animation.""" 151 | 152 | def __init__(self, animationId: str): 153 | # Animation id. 154 | self.animationId = animationId 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /gen.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # given a json schema enum, return an equivalent python enum 4 | def enum(type_): 5 | if type_["type"] == "string": 6 | return f'''{type_["id"]} = Enum("{type_["id"]}", "{" ".join(type_["enum"])}") 7 | {type_["id"]}.__doc__ = """{type_.get("description", "")}""" 8 | 9 | ''' 10 | else: 11 | print(type_) 12 | 1/0 13 | 14 | def typemap(typ): 15 | return { 16 | "string": "str", 17 | "integer": "int", 18 | "number": "float", 19 | "array": "List", 20 | "boolean": "bool", 21 | "any": "Any", 22 | # if we get here, the spec is for a dict 23 | "object": "dict", 24 | }[typ] 25 | 26 | def handle_properties(properties): 27 | dependencies = [] 28 | constructor_args = ["self"] 29 | args = [] 30 | 31 | for p in properties: 32 | if "$ref" in p: 33 | ref = p["$ref"].split(".") 34 | if len(ref) > 1: 35 | dependencies.append(ref[0]) 36 | ptype = f'"{p["$ref"]}"' 37 | else: 38 | # TODO: handle array sub-types here 39 | # TODO: handle enum sub-types above 40 | ptype = typemap(p["type"]) 41 | 42 | if p.get("type") == "object" and "properties" in p: 43 | # allow objects to pass as long as the spec means 44 | # dict and not a recursive object 45 | print(p) 46 | 1/0 47 | 48 | if p.get("optional"): 49 | constructor_args.append(f'{p["name"]}: {ptype}=None') 50 | else: 51 | constructor_args.append(f'{p["name"]}: {ptype}') 52 | 53 | if "description" in p: 54 | args.append(f'# {p["description"]}') 55 | 56 | args.append(f'self.{p["name"]} = {p["name"]}') 57 | 58 | return dependencies, constructor_args, args 59 | 60 | def object_(type_): 61 | if "properties" not in type_: 62 | return ([], f'class {type_["id"]}: pass\n\n') 63 | 64 | props = sorted(type_["properties"], key=lambda x: x.get("optional", False)) 65 | dependencies, constructor_args, args = handle_properties(props) 66 | 67 | docstring = f'\n """{type_["description"]}"""' if "description" in type_ else "" 68 | 69 | return (dependencies, f'''class {type_["id"]}:{docstring} 70 | def __init__({', '.join(constructor_args)}): 71 | ''' + '\n '.join(args) + "\n\n") 72 | 73 | def array(type_): 74 | # TODO: convert to ta tuple if there is minitems and maxitems? 75 | # ex: {'id': 'Quad', 'type': 'array', 'items': {'type': 'number'}, 'minItems': 8, 'maxItems': 8, 'description': 'An array of quad vertices, x immediately followed by y for each point, points clock-wise.', 'experimental': True} 76 | # should be a Tuple[float, float, float, float, float, float, float, float] ? 77 | # there's only this one case though, so I'm not going to stress it. 78 | out = [] 79 | if "type" in type_["items"]: 80 | eltype = typemap(type_["items"]["type"]) 81 | else: 82 | eltype = f'"{type_["items"]["$ref"]}"' 83 | 84 | if "description" in type_: 85 | out.append(f'# {type_["description"]}\n') 86 | if "description" in type_["items"]: 87 | out.append(f'# items: {type_["items"]["description"]}') 88 | out.append(f'{type_["id"]} = List[{eltype}]\n') 89 | 90 | return "".join(out) 91 | 92 | def simple(type_): 93 | out = [] 94 | ttype = typemap(type_["type"]) 95 | 96 | if "description" in type_: 97 | out.append(f'# {type_["description"]}\n') 98 | out.append(f'{type_["id"]} = {ttype}\n\n') 99 | 100 | return "".join(out) 101 | 102 | def command(cmd, domain): 103 | name = cmd["name"] 104 | docstr = f'\n """{cmd["description"]}"""\n' if "description" in cmd else '' 105 | 106 | props = sorted(cmd.get("parameters", []), key=lambda x: x.get("optional", False)) 107 | dependencies, constructor_args, args = handle_properties(props) 108 | 109 | if args: 110 | argcode = '\n ' + '\n '.join(args) + "\n\n" 111 | else: 112 | argcode = " pass" 113 | 114 | return f'''class {name}(ChromeCommand):{docstr} 115 | def __init__({", ".join(constructor_args)}):{argcode} 116 | 117 | ''' 118 | 119 | if __name__=="__main__": 120 | protocol = json.loads(open("protocol.json", ).read()) 121 | for domain in protocol["domains"]: 122 | name = domain["domain"] 123 | types = [] 124 | commands = [] 125 | dependencies = set() 126 | for type_ in domain.get("types", []): 127 | if "enum" in type_: types.append(enum(type_)) 128 | elif type_["type"] in ["string", "integer", "number"]: 129 | types.append(simple(type_)) 130 | elif type_["type"] == "object": 131 | deps, typeobj = object_(type_) 132 | dependencies.update(deps) 133 | types.append(typeobj) 134 | # TODO array 135 | elif type_["type"] == "array": types.append(array(type_)) 136 | else: 1/0 137 | 138 | for cmd in domain.get("commands", []): 139 | commands.append(command(cmd, domain["domain"])) 140 | 141 | mod = open(f"chrome_control/{name}.py", 'w') 142 | mod.write("""from enum import Enum 143 | from typing import Any, List 144 | 145 | from .base import ChromeCommand 146 | 147 | """) 148 | 149 | for dep in dependencies: 150 | mod.write(f'from . import {dep}\n') 151 | 152 | # TODO: objects have to be defined before being referenced fffuuuuu 153 | # so if class A has a parameter of type B, type B must appear 154 | # in the source file above type A 155 | mod.write('\n') 156 | mod.write(''.join(types)) 157 | 158 | mod.write(''.join(commands)) 159 | -------------------------------------------------------------------------------- /chrome_control/LayerTree.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import DOM 7 | 8 | # Unique Layer identifier. 9 | LayerId = str 10 | 11 | # Unique snapshot identifier. 12 | SnapshotId = str 13 | 14 | class ScrollRect: 15 | """Rectangle where scrolling happens on the main thread.""" 16 | def __init__(self, rect: "DOM.Rect", type: str): 17 | # Rectangle itself. 18 | self.rect = rect 19 | # Reason for rectangle to force scrolling on the main thread 20 | self.type = type 21 | 22 | class PictureTile: 23 | """Serialized fragment of layer picture along with its offset within the layer.""" 24 | def __init__(self, x: float, y: float, picture: str): 25 | # Offset from owning layer left boundary 26 | self.x = x 27 | # Offset from owning layer top boundary 28 | self.y = y 29 | # Base64-encoded snapshot data. 30 | self.picture = picture 31 | 32 | class Layer: 33 | """Information about a compositing layer.""" 34 | def __init__(self, layerId: "LayerId", offsetX: float, offsetY: float, width: float, height: float, paintCount: int, drawsContent: bool, parentLayerId: "LayerId"=None, backendNodeId: "DOM.BackendNodeId"=None, transform: List=None, anchorX: float=None, anchorY: float=None, anchorZ: float=None, invisible: bool=None, scrollRects: List=None): 35 | # The unique id for this layer. 36 | self.layerId = layerId 37 | # Offset from parent layer, X coordinate. 38 | self.offsetX = offsetX 39 | # Offset from parent layer, Y coordinate. 40 | self.offsetY = offsetY 41 | # Layer width. 42 | self.width = width 43 | # Layer height. 44 | self.height = height 45 | # Indicates how many time this layer has painted. 46 | self.paintCount = paintCount 47 | # Indicates whether this layer hosts any content, rather than being used for transform/scrolling purposes only. 48 | self.drawsContent = drawsContent 49 | # The id of parent (not present for root). 50 | self.parentLayerId = parentLayerId 51 | # The backend id for the node associated with this layer. 52 | self.backendNodeId = backendNodeId 53 | # Transformation matrix for layer, default is identity matrix 54 | self.transform = transform 55 | # Transform anchor point X, absent if no transform specified 56 | self.anchorX = anchorX 57 | # Transform anchor point Y, absent if no transform specified 58 | self.anchorY = anchorY 59 | # Transform anchor point Z, absent if no transform specified 60 | self.anchorZ = anchorZ 61 | # Set if layer is not visible. 62 | self.invisible = invisible 63 | # Rectangles scrolling on main thread only. 64 | self.scrollRects = scrollRects 65 | 66 | # Array of timings, one per paint step. 67 | # items: A time in seconds since the end of previous step (for the first step, time since painting started)PaintProfile = List[float] 68 | class enable(ChromeCommand): 69 | """Enables compositing tree inspection.""" 70 | 71 | def __init__(self): pass 72 | 73 | class disable(ChromeCommand): 74 | """Disables compositing tree inspection.""" 75 | 76 | def __init__(self): pass 77 | 78 | class compositingReasons(ChromeCommand): 79 | """Provides the reasons why the given layer was composited.""" 80 | 81 | def __init__(self, layerId: "LayerId"): 82 | # The id of the layer for which we want to get the reasons it was composited. 83 | self.layerId = layerId 84 | 85 | 86 | 87 | class makeSnapshot(ChromeCommand): 88 | """Returns the layer snapshot identifier.""" 89 | 90 | def __init__(self, layerId: "LayerId"): 91 | # The id of the layer. 92 | self.layerId = layerId 93 | 94 | 95 | 96 | class loadSnapshot(ChromeCommand): 97 | """Returns the snapshot identifier.""" 98 | 99 | def __init__(self, tiles: List): 100 | # An array of tiles composing the snapshot. 101 | self.tiles = tiles 102 | 103 | 104 | 105 | class releaseSnapshot(ChromeCommand): 106 | """Releases layer snapshot captured by the back-end.""" 107 | 108 | def __init__(self, snapshotId: "SnapshotId"): 109 | # The id of the layer snapshot. 110 | self.snapshotId = snapshotId 111 | 112 | 113 | 114 | class profileSnapshot(ChromeCommand): 115 | def __init__(self, snapshotId: "SnapshotId", minRepeatCount: int=None, minDuration: float=None, clipRect: "DOM.Rect"=None): 116 | # The id of the layer snapshot. 117 | self.snapshotId = snapshotId 118 | # The maximum number of times to replay the snapshot (1, if not specified). 119 | self.minRepeatCount = minRepeatCount 120 | # The minimum duration (in seconds) to replay the snapshot. 121 | self.minDuration = minDuration 122 | # The clip rectangle to apply when replaying the snapshot. 123 | self.clipRect = clipRect 124 | 125 | 126 | 127 | class replaySnapshot(ChromeCommand): 128 | """Replays the layer snapshot and returns the resulting bitmap.""" 129 | 130 | def __init__(self, snapshotId: "SnapshotId", fromStep: int=None, toStep: int=None, scale: float=None): 131 | # The id of the layer snapshot. 132 | self.snapshotId = snapshotId 133 | # The first step to replay from (replay from the very start if not specified). 134 | self.fromStep = fromStep 135 | # The last step to replay to (replay till the end if not specified). 136 | self.toStep = toStep 137 | # The scale to apply while replaying (defaults to 1). 138 | self.scale = scale 139 | 140 | 141 | 142 | class snapshotCommandLog(ChromeCommand): 143 | """Replays the layer snapshot and returns canvas log.""" 144 | 145 | def __init__(self, snapshotId: "SnapshotId"): 146 | # The id of the layer snapshot. 147 | self.snapshotId = snapshotId 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /chrome_control/Accessibility.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any, List 3 | 4 | from .base import ChromeCommand 5 | 6 | from . import DOM 7 | 8 | # Unique accessibility node identifier. 9 | AXNodeId = str 10 | 11 | AXValueType = Enum("AXValueType", "boolean tristate booleanOrUndefined idref idrefList integer node nodeList number string computedString token tokenList domRelation role internalRole valueUndefined") 12 | AXValueType.__doc__ = """Enum of possible property types.""" 13 | 14 | AXValueSourceType = Enum("AXValueSourceType", "attribute implicit style contents placeholder relatedElement") 15 | AXValueSourceType.__doc__ = """Enum of possible property sources.""" 16 | 17 | AXValueNativeSourceType = Enum("AXValueNativeSourceType", "figcaption label labelfor labelwrapped legend tablecaption title other") 18 | AXValueNativeSourceType.__doc__ = """Enum of possible native property sources (as a subtype of a particular AXValueSourceType).""" 19 | 20 | class AXValueSource: 21 | """A single source for a computed AX property.""" 22 | def __init__(self, type: "AXValueSourceType", value: "AXValue"=None, attribute: str=None, attributeValue: "AXValue"=None, superseded: bool=None, nativeSource: "AXValueNativeSourceType"=None, nativeSourceValue: "AXValue"=None, invalid: bool=None, invalidReason: str=None): 23 | # What type of source this is. 24 | self.type = type 25 | # The value of this property source. 26 | self.value = value 27 | # The name of the relevant attribute, if any. 28 | self.attribute = attribute 29 | # The value of the relevant attribute, if any. 30 | self.attributeValue = attributeValue 31 | # Whether this source is superseded by a higher priority source. 32 | self.superseded = superseded 33 | # The native markup source for this value, e.g. a