|
| 1 | +sessionId support for DevToolsProtocol method call and event |
| 2 | +=== |
| 3 | + |
| 4 | +# Background |
| 5 | +A web page can have multiple DevToolsProtocol targets. Besides the default target for the top page, there are separate targets for iframes from different origin and web workers. |
| 6 | + |
| 7 | +The underlying DevToolsProtocol supports interaction with other targets by attaching to them and then using sessionId of the attachment in DevToolsProtocol method call message. |
| 8 | +The received DevToolsProtocol event messages also have sessionId field to indicate which target the event comes from. |
| 9 | +See [DevToolsProtocol Target domain](https://chromedevtools.github.io/devtools-protocol/tot/Target/) for more details. |
| 10 | + |
| 11 | +However, the current WebView2 DevToolsProtocol APIs like [CallDevToolsProtocolMethod](https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2#calldevtoolsprotocolmethod) |
| 12 | +and [DevToolsProtocolEventReceived](https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2devtoolsprotocoleventreceivedeventargs) |
| 13 | +doesn't support sessionId. |
| 14 | + |
| 15 | +To support interaction with different parts of the page, we are adding support for specifying sessionId for DevToolsProtocol method call API |
| 16 | +and retrieving sessionId for received DevToolsProtocol events. |
| 17 | + |
| 18 | +# Conceptual pages (How To) |
| 19 | + |
| 20 | +To use the sessionId support, you must attach to targets with with `flatten` set as `true` when calling `Target.attachToTarget` or `Target.setAutoAttach`. |
| 21 | + |
| 22 | +You can listen to `Target.attachedToTarget` and `Target.detachedFromTarget` events to manage the sessionId for targets, and listen to `Target.targetInfoChanged` event to update target info like url of a target. |
| 23 | + |
| 24 | +There is also some nuance for DevToolsProtocol's target management. If you are interested in only top page and iframes from different origins on the page, it will be simple and straight forward. All related methods and events like `Target.getTargets`, `Target.attachToTarget`, and `Target.targetCreated` event work as expected. |
| 25 | + |
| 26 | +However, dedicated web workers are not returned from `'Target.getTargets'`, and you have to call DevToolsProtocol method `Target.setAutoAttach` to be able to attach to them. |
| 27 | + |
| 28 | +And shared worker is separate from any page or iframe target, and therefore will not be auto attached. You have to call `Target.attachToTarget` to attach to them. The shared workers can be enumerated with `Target.getTargets`. They are also discoverable, that is, you can call `Target.setDiscoverTargets` to receive `Target.targetCreated` event when a shared worker is created. |
| 29 | + |
| 30 | +To summarize, there are two ways of finding the targets and neither covers all 3 scenarios. |
| 31 | + |
| 32 | +| | Web pages | Dedicated web workers | Shared workers | |
| 33 | +| --- | --- | --- | --- | |
| 34 | +| Target.getTargets, Target.setDiscoverTargets, Target.targetCreated, Target.attachToTarget | ✔ | | ✔ | |
| 35 | +| Target.setAutoAttach, Target.attachedToTarget | ✔ | ✔ | | |
| 36 | + |
| 37 | +# Examples |
| 38 | + |
| 39 | +The example below illustrates how to collect messages logged by console.log calls by JavaScipt code from various parts of the web page, including dedicated web worker. |
| 40 | + |
| 41 | +## Win32 C++ |
| 42 | +```cpp |
| 43 | + |
| 44 | +void MyApp::HandleDevToolsProtocalPTargets() |
| 45 | +{ |
| 46 | + // The sessions and targets descriptions are tracked by 2 maps: |
| 47 | + // SessionId to TargetId map: |
| 48 | + // std::map<std::wstring, std::wstring> m_devToolsSessionMap; |
| 49 | + // TargetId to description map, where description is "<target type>,<target url>". |
| 50 | + // std::map<std::wstring, std::wstring> m_devToolsTargetInfoMap; |
| 51 | + // GetJSONStringField is a helper function that can retrieve a string field from a json message. |
| 52 | + |
| 53 | + wil::com_ptr<ICoreWebView2DevToolsProtocolEventReceiver> receiver; |
| 54 | + // Listen to Runtime.consoleAPICalled event which is triggered when console.log is called by script code. |
| 55 | + CHECK_FAILURE( |
| 56 | + m_webView->GetDevToolsProtocolEventReceiver(L"Runtime.consoleAPICalled", &receiver)); |
| 57 | + CHECK_FAILURE(receiver->add_DevToolsProtocolEventReceived( |
| 58 | + Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>( |
| 59 | + [this]( |
| 60 | + ICoreWebView2* sender, |
| 61 | + ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT |
| 62 | + { |
| 63 | + // Get console.log message details and which target it comes from |
| 64 | + wil::unique_cotaskmem_string parameterObjectAsJson; |
| 65 | + CHECK_FAILURE(args->get_ParameterObjectAsJson(¶meterObjectAsJson)); |
| 66 | + std::wstring eventSource; |
| 67 | + std::wstring eventDetails = parameterObjectAsJson.get(); |
| 68 | + wil::com_ptr<ICoreWebView2DevToolsProtocolEventReceivedEventArgs2> args2; |
| 69 | + if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&args2)))) |
| 70 | + { |
| 71 | + wil::unique_cotaskmem_string sessionId; |
| 72 | + CHECK_FAILURE(args2->get_SessionId(&sessionId)); |
| 73 | + if (sessionId.get() && *sessionId.get()) |
| 74 | + { |
| 75 | + std::wstring targetId = m_devToolsSessionMap[sessionId.get()]; |
| 76 | + eventSource = m_devToolsTargetDescriptionMap[targetId]; |
| 77 | + } |
| 78 | + } |
| 79 | + // App code to log these events. |
| 80 | + LogConsoleLogMessage(eventSource, eventDetails); |
| 81 | + return S_OK; |
| 82 | + }) |
| 83 | + .Get(), |
| 84 | + &m_consoleAPICalledToken)); |
| 85 | + receiver.reset(); |
| 86 | + |
| 87 | + // Track Target and session info via CDP events. |
| 88 | + CHECK_FAILURE( |
| 89 | + m_webView->GetDevToolsProtocolEventReceiver(L"Target.attachedToTarget", &receiver)); |
| 90 | + CHECK_FAILURE(receiver->add_DevToolsProtocolEventReceived( |
| 91 | + Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>( |
| 92 | + [this]( |
| 93 | + ICoreWebView2* sender, |
| 94 | + ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT |
| 95 | + { |
| 96 | + // A new target is attached, add its info to maps. |
| 97 | + wil::unique_cotaskmem_string jsonMessage; |
| 98 | + CHECK_FAILURE(args->get_ParameterObjectAsJson(&jsonMessage)); |
| 99 | + std::wstring sessionId = GetJSONStringField(jsonMessage.get(), L"sessionId"); |
| 100 | + std::wstring targetId = GetJSONStringField(jsonMessage.get(), L"targetId"); |
| 101 | + m_devToolsSessionMap[sessionId] = targetId; |
| 102 | + std::wstring type = GetJSONStringField(jsonMessage.get(), L"type"); |
| 103 | + std::wstring url = GetJSONStringField(jsonMessage.get(), L"url"); |
| 104 | + m_devToolsTargetDescriptionMap[targetId] = type + L"," + url; |
| 105 | + wil::com_ptr<ICoreWebView2_10> webview2 = m_webView.try_query<ICoreWebView2_10>(); |
| 106 | + if (webview2) |
| 107 | + { |
| 108 | + // Auto attach to targets further created from this target. |
| 109 | + webview2->CallDevToolsProtocolMethodForSession( |
| 110 | + sessionId.c_str(), L"Target.setAutoAttach", |
| 111 | + LR"({"autoAttach":true,"waitForDebuggerOnStart":false,"flatten":true})", |
| 112 | + nullptr); |
| 113 | + // Enable Runtime events for the target to receive Runtime.consoleAPICalled from it. |
| 114 | + webview2->CallDevToolsProtocolMethodForSession( |
| 115 | + sessionId.c_str(), L"Runtime.enable", L"{}", nullptr); |
| 116 | + } |
| 117 | + return S_OK; |
| 118 | + }) |
| 119 | + .Get(), |
| 120 | + &m_targetAttachedToken)); |
| 121 | + receiver.reset(); |
| 122 | + CHECK_FAILURE( |
| 123 | + m_webView->GetDevToolsProtocolEventReceiver(L"Target.detachedFromTarget", &receiver)); |
| 124 | + CHECK_FAILURE(receiver->add_DevToolsProtocolEventReceived( |
| 125 | + Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>( |
| 126 | + [this]( |
| 127 | + ICoreWebView2* sender, |
| 128 | + ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT |
| 129 | + { |
| 130 | + // A target is detached, remove it from the maps. |
| 131 | + wil::unique_cotaskmem_string jsonMessage; |
| 132 | + CHECK_FAILURE(args->get_ParameterObjectAsJson(&jsonMessage)); |
| 133 | + std::wstring sessionId = GetJSONStringField(jsonMessage.get(), L"sessionId"); |
| 134 | + auto session = m_devToolsSessionMap.find(sessionId); |
| 135 | + if (session != m_devToolsSessionMap.end()) |
| 136 | + { |
| 137 | + m_devToolsTargetDescriptionMap.erase(session->second); |
| 138 | + m_devToolsSessionMap.erase(session); |
| 139 | + } |
| 140 | + return S_OK; |
| 141 | + }) |
| 142 | + .Get(), |
| 143 | + &m_targetDetachedToken)); |
| 144 | + receiver.reset(); |
| 145 | + CHECK_FAILURE( |
| 146 | + m_webView->GetDevToolsProtocolEventReceiver(L"Target.targetInfoChanged", &receiver)); |
| 147 | + CHECK_FAILURE(receiver->add_DevToolsProtocolEventReceived( |
| 148 | + Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>( |
| 149 | + [this]( |
| 150 | + ICoreWebView2* sender, |
| 151 | + ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT |
| 152 | + { |
| 153 | + // A target's info like url changed, update it in the target description map. |
| 154 | + wil::unique_cotaskmem_string jsonMessage; |
| 155 | + CHECK_FAILURE(args->get_ParameterObjectAsJson(&jsonMessage)); |
| 156 | + std::wstring targetId = GetJSONStringField(jsonMessage.get(), L"targetId"); |
| 157 | + if (m_devToolsTargetDescriptionMap.find(targetId) != |
| 158 | + m_devToolsTargetDescriptionMap.end()) |
| 159 | + { |
| 160 | + // This is a target that we are interested in, update description. |
| 161 | + std::wstring type = GetJSONStringField(jsonMessage.get(), L"type"); |
| 162 | + std::wstring url = GetJSONStringField(jsonMessage.get(), L"url"); |
| 163 | + m_devToolsTargetDescriptionMap[targetId] = type + L"," + url; |
| 164 | + } |
| 165 | + return S_OK; |
| 166 | + }) |
| 167 | + .Get(), |
| 168 | + &m_targetInfoChangedToken)); |
| 169 | + // Enable Runtime events for the default target of top page to receive Runtime.consoleAPICalled events, which is fired when console.log is called. |
| 170 | + m_webView->CallDevToolsProtocolMethod(L"Runtime.enable", L"{}", nullptr); |
| 171 | + // Auto attach to iframe and dedicated worker targets created from the default target of top page. |
| 172 | + m_webView->CallDevToolsProtocolMethod( |
| 173 | + L"Target.setAutoAttach", |
| 174 | + LR"({"autoAttach":true,"waitForDebuggerOnStart":false,"flatten":true})", nullptr); |
| 175 | +} |
| 176 | +``` |
| 177 | +## WinRT and .NET |
| 178 | +```c# |
| 179 | +// The sessions and targets descriptions are tracked by 2 dictionaries: |
| 180 | +// SessionId to TargetId dictionary: m_devToolsSessionMap; |
| 181 | +// TargetId to description dictionary, where description is "<target type>,<target url>": m_devToolsTargetInfoMap |
| 182 | +// GetJSONStringField is a helper function that can retrieve a string field from a json message. |
| 183 | +
|
| 184 | +private void CoreWebView2_ConsoleAPICalled(CoreWebView2 sender, CoreWebView2DevToolsProtocolEventReceivedEventArgs args) |
| 185 | +{ |
| 186 | + // Figure out which target the console.log comes from |
| 187 | + string eventSource; |
| 188 | + string sessionId = args.SessionId; |
| 189 | + if (sessionId.Length > 0) |
| 190 | + { |
| 191 | + string targetId = m_devToolsSessionMap[sessionId]; |
| 192 | + eventSource = m_devToolsTargetDescriptionMap[targetId]; |
| 193 | + } |
| 194 | + // App code to log these events. |
| 195 | + LogConsoleLogMessage(eventSource, args.ParameterObjectAsJson); |
| 196 | +} |
| 197 | + |
| 198 | +private void CoreWebView2_AttachedToTarget(CoreWebView2 sender, CoreWebView2DevToolsProtocolEventReceivedEventArgs args) |
| 199 | +{ |
| 200 | + // A new target is attached, add its info to maps. |
| 201 | + string jsonMessage = args.ParameterObjectAsJson; |
| 202 | + string sessionId = GetJSONStringField(jsonMessage, L"sessionId"); |
| 203 | + string targetId = GetJSONStringField(jsonMessage, L"targetId"); |
| 204 | + m_devToolsSessionMap[sessionId] = targetId; |
| 205 | + string type = GetJSONStringField(jsonMessage, L"type"); |
| 206 | + string url = GetJSONStringField(jsonMessage, L"url"); |
| 207 | + m_devToolsTargetDescriptionMap[targetId] = type + L"," + url; |
| 208 | + // Auto attach to targets further created from this target. |
| 209 | + _ = m_webview.CallDevToolsProtocolMethodAsync("Target.setAutoAttach", |
| 210 | + @"{""autoAttach"":true,""waitForDebuggerOnStart"":false,""flatten"":true}", |
| 211 | + sessionId); |
| 212 | + // Enable Runtime events to receive Runtime.consoleAPICalled from the target, which is triggered by console.log calls. |
| 213 | + m_webview.CallDevToolsProtocolMethodAsync("Runtime.enable", "{}", sessionId); |
| 214 | +} |
| 215 | + |
| 216 | +private void CoreWebView2_DetachedFromTarget(CoreWebView2 sender, CoreWebView2DevToolsProtocolEventReceivedEventArgs args) |
| 217 | +{ |
| 218 | + // A target is detached, remove it from the maps |
| 219 | + string jsonMessage = args.ParameterObjectAsJson; |
| 220 | + string sessionId = GetJSONStringField(jsonMessage, L"sessionId"); |
| 221 | + if (m_devToolsSessionMap.ContainsKey(sessionId)) |
| 222 | + { |
| 223 | + m_devToolsTargetDescriptionMap.Remove(m_devToolsSessionMap[sessionId]); |
| 224 | + m_devToolsSessionMap.Remove(sessionId); |
| 225 | + } |
| 226 | +} |
| 227 | + |
| 228 | +private void CoreWebView2_TargetInfoChanged(CoreWebView2 sender, CoreWebView2DevToolsProtocolEventReceivedEventArgs args) |
| 229 | +{ |
| 230 | + // A target's info like url changed, update it in the target description map. |
| 231 | + string jsonMessage = args.ParameterObjectAsJson; |
| 232 | + string targetId = GetJSONStringField(jsonMessage, L"targetId"); |
| 233 | + if (m_devToolsTargetDescriptionMap.ContainsKey(targetId)) |
| 234 | + { |
| 235 | + // This is a target that we are interested in, update description. |
| 236 | + string type = GetJSONStringField(jsonMessage, L"type"); |
| 237 | + string url = GetJSONStringField(jsonMessage, L"url"); |
| 238 | + m_devToolsTargetDescriptionMap[targetId] = type + L"," + url; |
| 239 | + } |
| 240 | +} |
| 241 | + private void HandleDevToolsProtocalPTargets() |
| 242 | + { |
| 243 | + m_webview.GetDevToolsProtocolEventReceiver("Runtime.consoleAPICalled").DevToolsProtocolEventReceived += CoreWebView2_ConsoleAPICalled; |
| 244 | + m_webview.GetDevToolsProtocolEventReceiver("Target.attachedToTarget").DevToolsProtocolEventReceived += CoreWebView2_AttachedToTarget; |
| 245 | + m_webview.GetDevToolsProtocolEventReceiver("Target.detachedFromTarget").DevToolsProtocolEventReceived += CoreWebView2_DetachedFromTarget; |
| 246 | + m_webview.GetDevToolsProtocolEventReceiver("Target.targetInfoChanged").DevToolsProtocolEventReceived += CoreWebView2_TargetInfoChanged; |
| 247 | + // Enable Runtime events for the default target of top page to receive Runtime.consoleAPICalled events, which is fired when console.log is called. |
| 248 | + _ = m_webview.CallDevToolsProtocolMethodAsync("Runtime.enable", "{}"); |
| 249 | + // Auto attach to iframe and dedicated worker targets created from the default target of top page. |
| 250 | + _ = m_webview.CallDevToolsProtocolMethodAsync("Target.setAutoAttach", |
| 251 | + @"{""autoAttach"":true,""waitForDebuggerOnStart"":false,""flatten"":true}"); |
| 252 | + } |
| 253 | +``` |
| 254 | + |
| 255 | +# API Details |
| 256 | +## Win32 C++ |
| 257 | +``` |
| 258 | +interface ICoreWebView2_10 : IUnknown { |
| 259 | + /// Runs an asynchronous `DevToolsProtocol` method for a specific session of |
| 260 | + /// an attached target. |
| 261 | + /// There could be multiple `DevToolsProtocol` targets in a WebView. |
| 262 | + /// Besides the top level page, iframes from different origin and web workers |
| 263 | + /// are also separate targets. Attaching to these targets allows interaction with them. |
| 264 | + /// These attachments to the targets are sessions and are identified by sessionId. |
| 265 | + /// To use this API, you should set `flatten` parameter to true when calling |
| 266 | + /// `Target.attachToTarget` or `Target.setAutoAttach` `DevToolsProtocol` method. |
| 267 | + /// Using `Target.setAutoAttach` is recommended as that would allow you to attach |
| 268 | + /// to dedicated worker target, which is not discoverable via other APIs like |
| 269 | + /// `Target.getTargets`. |
| 270 | + /// For more information about targets and sessions, navigate to |
| 271 | + /// \[DevTools Protocol Viewer\]\[GithubChromedevtoolsDevtoolsProtocolTotTarget\]. |
| 272 | + /// For more information about available methods, navigate to |
| 273 | + /// \[DevTools Protocol Viewer\]\[GithubChromedevtoolsDevtoolsProtocolTot\] |
| 274 | + /// The `sessionId` parameter is the sessionId for an attached target. |
| 275 | + /// nullptr or empty string is treated as the session for the default target for the top page. |
| 276 | + /// The `methodName` parameter is the full name of the method in the |
| 277 | + /// `{domain}.{method}` format. The `parametersAsJson` parameter is a JSON |
| 278 | + /// formatted string containing the parameters for the corresponding method. |
| 279 | + /// The `Invoke` method of the `handler` is run when the method |
| 280 | + /// asynchronously completes. `Invoke` is run with the return object of the |
| 281 | + /// method as a JSON string. |
| 282 | + /// |
| 283 | + /// \[GithubChromedevtoolsDevtoolsProtocolTot\]: https://chromedevtools.github.io/devtools-protocol/tot "latest (tip-of-tree) protocol - Chrome DevTools Protocol | GitHub" |
| 284 | + /// \[GithubChromedevtoolsDevtoolsProtocolTotTarget\]: https://chromedevtools.github.io/devtools-protocol/tot/Target "Chrome DevTools Protocol - Target domain" |
| 285 | +
|
| 286 | + HRESULT CallDevToolsProtocolMethodForSession( |
| 287 | + [in] LPCWSTR sessionId, |
| 288 | + [in] LPCWSTR methodName, |
| 289 | + [in] LPCWSTR parametersAsJson, |
| 290 | + [in] ICoreWebView2CallDevToolsProtocolMethodCompletedHandler* handler); |
| 291 | +} |
| 292 | +
|
| 293 | +interface ICoreWebView2DevToolsProtocolEventReceivedEventArgs2 : IUnknown { |
| 294 | +
|
| 295 | + /// The sessionId of the target where the event originates from. |
| 296 | + /// Empty string is returned as sessionId if the event comes from the default session for the top page. |
| 297 | + /// \snippet ScriptComponent.cpp DevToolsProtocolEventReceivedSessionId |
| 298 | + [propget] HRESULT SessionId([out, retval] LPWSTR* sessionId); |
| 299 | +} |
| 300 | +``` |
| 301 | + |
| 302 | +## WinRT and .NET |
| 303 | +```c# |
| 304 | +namespace Microsoft.Web.WebView2.Core |
| 305 | +{ |
| 306 | + runtimeclass CoreWebView2 |
| 307 | + { |
| 308 | + // ... |
| 309 | + |
| 310 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2_10")] |
| 311 | + { |
| 312 | + // ICoreWebView2_10 members |
| 313 | + // This is an overload for: public async Task<string> CallDevToolsProtocolMethodAsync(string methodName, string parametersAsJson); |
| 314 | + public async Task<string> CallDevToolsProtocolMethodAsync(string methodName, string parametersAsJson, string sessionId); |
| 315 | + } |
| 316 | + } |
| 317 | + |
| 318 | + runtimeclass CoreWebView2DevToolsProtocolEventReceivedEventArgs |
| 319 | + { |
| 320 | + // ... |
| 321 | +
|
| 322 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2DevToolsProtocolEventReceivedEventArgs2")] |
| 323 | + { |
| 324 | + String SessionId { get; }; |
| 325 | + } |
| 326 | + } |
| 327 | +} |
| 328 | +``` |
0 commit comments