Skip to content

Commit d4c3262

Browse files
authored
Merge pull request #2006 from MicrosoftEdge/api-cdp-sessionId-draft
CDP sessionId Support
2 parents a56f59d + 936fa38 commit d4c3262

1 file changed

Lines changed: 328 additions & 0 deletions

File tree

specs/CDPSessionIdSupport.md

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
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(&parameterObjectAsJson));
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

Comments
 (0)