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