"Fossies" - the Fresh Open Source Software Archive 
Member "selenium-selenium-4.8.1/dotnet/src/webdriver/DevTools/DevToolsSession.cs" (17 Feb 2023, 28007 Bytes) of package /linux/www/selenium-selenium-4.8.1.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C# source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "DevToolsSession.cs" see the
Fossies "Dox" file reference documentation and the last
Fossies "Diffs" side-by-side code changes report:
4.5.0_vs_4.6.0.
1 // <copyright file="DevToolsSession.cs" company="WebDriver Committers">
2 // Licensed to the Software Freedom Conservancy (SFC) under one
3 // or more contributor license agreements. See the NOTICE file
4 // distributed with this work for additional information
5 // regarding copyright ownership. The SFC licenses this file
6 // to you under the Apache License, Version 2.0 (the "License");
7 // you may not use this file except in compliance with the License.
8 // You may obtain a copy of the License at
9 //
10 // http://www.apache.org/licenses/LICENSE-2.0
11 //
12 // Unless required by applicable law or agreed to in writing, software
13 // distributed under the License is distributed on an "AS IS" BASIS,
14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 // See the License for the specific language governing permissions and
16 // limitations under the License.
17 // </copyright>
18
19 using System;
20 using System.Collections.Concurrent;
21 using System.Globalization;
22 using System.IO;
23 using System.Net.Http;
24 using System.Net.WebSockets;
25 using System.Text;
26 using System.Threading;
27 using System.Threading.Tasks;
28 using Newtonsoft.Json;
29 using Newtonsoft.Json.Linq;
30
31 namespace OpenQA.Selenium.DevTools
32 {
33 /// <summary>
34 /// Represents a WebSocket connection to a running DevTools instance that can be used to send
35 /// commands and recieve events.
36 ///</summary>
37 public class DevToolsSession : IDevToolsSession
38 {
39 public const int AutoDetectDevToolsProtocolVersion = 0;
40
41 private readonly string debuggerEndpoint;
42 private string websocketAddress;
43 private readonly TimeSpan openConnectionWaitTimeSpan = TimeSpan.FromSeconds(30);
44 private readonly TimeSpan closeConnectionWaitTimeSpan = TimeSpan.FromSeconds(2);
45
46 private bool isDisposed = false;
47 private string attachedTargetId;
48
49 private ClientWebSocket sessionSocket;
50 private ConcurrentDictionary<long, DevToolsCommandData> pendingCommands = new ConcurrentDictionary<long, DevToolsCommandData>();
51 private long currentCommandId = 0;
52
53 private DevToolsDomains domains;
54
55 private CancellationTokenSource receiveCancellationToken;
56 private Task receiveTask;
57
58 /// <summary>
59 /// Initializes a new instance of the DevToolsSession class, using the specified WebSocket endpoint.
60 /// </summary>
61 /// <param name="endpointAddress"></param>
62 public DevToolsSession(string endpointAddress)
63 {
64 if (string.IsNullOrWhiteSpace(endpointAddress))
65 {
66 throw new ArgumentNullException(nameof(endpointAddress));
67 }
68
69 this.CommandTimeout = TimeSpan.FromSeconds(5);
70 this.debuggerEndpoint = endpointAddress;
71 if (endpointAddress.StartsWith("ws:"))
72 {
73 this.websocketAddress = endpointAddress;
74 }
75 }
76
77 /// <summary>
78 /// Event raised when the DevToolsSession logs informational messages.
79 /// </summary>
80 public event EventHandler<DevToolsSessionLogMessageEventArgs> LogMessage;
81
82 /// <summary>
83 /// Event raised an event notification is received from the DevTools session.
84 /// </summary>
85 public event EventHandler<DevToolsEventReceivedEventArgs> DevToolsEventReceived;
86
87 /// <summary>
88 /// Gets or sets the time to wait for a command to complete. Default is 5 seconds.
89 /// </summary>
90 public TimeSpan CommandTimeout { get; set; }
91
92 /// <summary>
93 /// Gets or sets the active session ID of the connection.
94 /// </summary>
95 public string ActiveSessionId { get; private set; }
96
97 /// <summary>
98 /// Gets the endpoint address of the session.
99 /// </summary>
100 public string EndpointAddress => this.websocketAddress;
101
102 /// <summary>
103 /// Gets the version-independent domain implementation for this Developer Tools connection
104 /// </summary>
105 public DevToolsDomains Domains => this.domains;
106
107 /// <summary>
108 /// Gets the version-specific implementation of domains for this DevTools session.
109 /// </summary>
110 /// <typeparam name="T">
111 /// A <see cref="DevToolsSessionDomains"/> object containing the version-specific DevTools Protocol domain implementations.</typeparam>
112 /// <returns>The version-specific DevTools Protocol domain implementation.</returns>
113 public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
114 {
115 T versionSpecificDomains = this.domains.VersionSpecificDomains as T;
116 if (versionSpecificDomains == null)
117 {
118 string errorTemplate = "The type is invalid for conversion. You requested domains of type '{0}', but the version-specific domains for this session are '{1}'";
119 string exceptionMessage = string.Format(CultureInfo.InvariantCulture, errorTemplate, typeof(T).ToString(), this.domains.GetType().ToString());
120 throw new InvalidOperationException(exceptionMessage);
121 }
122
123 return versionSpecificDomains;
124 }
125
126 /// <summary>
127 /// Sends the specified command and returns the associated command response.
128 /// </summary>
129 /// <typeparam name="TCommand">A command object implementing the <see cref="ICommand"/> interface.</typeparam>
130 /// <param name="command">The command to be sent.</param>
131 /// <param name="cancellationToken">A CancellationToken object to allow for cancellation of the command.</param>
132 /// <param name="millisecondsTimeout">The execution timeout of the command in milliseconds.</param>
133 /// <param name="throwExceptionIfResponseNotReceived"><see langword="true"/> to throw an exception if a response is not received; otherwise, <see langword="false"/>.</param>
134 /// <returns>The command response object implementing the <see cref="ICommandResponse{T}"/> interface.</returns>
135 public async Task<ICommandResponse<TCommand>> SendCommand<TCommand>(TCommand command, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
136 where TCommand : ICommand
137 {
138 if (command == null)
139 {
140 throw new ArgumentNullException(nameof(command));
141 }
142
143 var result = await SendCommand(command.CommandName, JToken.FromObject(command), cancellationToken, millisecondsTimeout, throwExceptionIfResponseNotReceived);
144
145 if (result == null)
146 {
147 return null;
148 }
149
150 if (!this.domains.VersionSpecificDomains.ResponseTypeMap.TryGetCommandResponseType<TCommand>(out Type commandResponseType))
151 {
152 throw new InvalidOperationException($"Type {typeof(TCommand)} does not correspond to a known command response type.");
153 }
154
155 return result.ToObject(commandResponseType) as ICommandResponse<TCommand>;
156 }
157
158 /// <summary>
159 /// Sends the specified command and returns the associated command response.
160 /// </summary>
161 /// <typeparam name="TCommand"></typeparam>
162 /// <typeparam name="TCommandResponse"></typeparam>
163 /// <typeparam name="TCommand">A command object implementing the <see cref="ICommand"/> interface.</typeparam>
164 /// <param name="cancellationToken">A CancellationToken object to allow for cancellation of the command.</param>
165 /// <param name="millisecondsTimeout">The execution timeout of the command in milliseconds.</param>
166 /// <param name="throwExceptionIfResponseNotReceived"><see langword="true"/> to throw an exception if a response is not received; otherwise, <see langword="false"/>.</param>
167 /// <returns>The command response object implementing the <see cref="ICommandResponse{T}"/> interface.</returns>
168 public async Task<TCommandResponse> SendCommand<TCommand, TCommandResponse>(TCommand command, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
169 where TCommand : ICommand
170 where TCommandResponse : ICommandResponse<TCommand>
171 {
172 if (command == null)
173 {
174 throw new ArgumentNullException(nameof(command));
175 }
176
177 var result = await SendCommand(command.CommandName, JToken.FromObject(command), cancellationToken, millisecondsTimeout, throwExceptionIfResponseNotReceived);
178
179 if (result == null)
180 {
181 return default(TCommandResponse);
182 }
183
184 return result.ToObject<TCommandResponse>();
185 }
186
187 /// <summary>
188 /// Returns a JToken based on a command created with the specified command name and params.
189 /// </summary>
190 /// <param name="commandName">The name of the command to send.</param>
191 /// <param name="commandParameters">The parameters of the command as a JToken object</param>
192 /// <param name="cancellationToken">A CancellationToken object to allow for cancellation of the command.</param>
193 /// <param name="millisecondsTimeout">The execution timeout of the command in milliseconds.</param>
194 /// <param name="throwExceptionIfResponseNotReceived"><see langword="true"/> to throw an exception if a response is not received; otherwise, <see langword="false"/>.</param>
195 /// <returns>The command response object implementing the <see cref="ICommandResponse{T}"/> interface.</returns>
196 //[DebuggerStepThrough]
197 public async Task<JToken> SendCommand(string commandName, JToken commandParameters, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
198 {
199 if (millisecondsTimeout.HasValue == false)
200 {
201 millisecondsTimeout = Convert.ToInt32(CommandTimeout.TotalMilliseconds);
202 }
203
204 if (this.attachedTargetId == null)
205 {
206 LogTrace("Session not currently attached to a target; reattaching");
207 await this.InitializeSession();
208 }
209
210 var message = new DevToolsCommandData(Interlocked.Increment(ref this.currentCommandId), this.ActiveSessionId, commandName, commandParameters);
211
212 if (this.sessionSocket != null && this.sessionSocket.State == WebSocketState.Open)
213 {
214 LogTrace("Sending {0} {1}: {2}", message.CommandId, message.CommandName, commandParameters.ToString());
215
216 var contents = JsonConvert.SerializeObject(message);
217 var contentBuffer = Encoding.UTF8.GetBytes(contents);
218
219 this.pendingCommands.TryAdd(message.CommandId, message);
220 await this.sessionSocket.SendAsync(new ArraySegment<byte>(contentBuffer), WebSocketMessageType.Text, true, cancellationToken);
221
222 var responseWasReceived = await Task.Run(() => message.SyncEvent.Wait(millisecondsTimeout.Value, cancellationToken));
223
224 if (!responseWasReceived && throwExceptionIfResponseNotReceived)
225 {
226 throw new InvalidOperationException($"A command response was not received: {commandName}");
227 }
228
229 DevToolsCommandData modified;
230 if (this.pendingCommands.TryRemove(message.CommandId, out modified))
231 {
232 if (modified.IsError)
233 {
234 var errorMessage = modified.Result.Value<string>("message");
235 var errorData = modified.Result.Value<string>("data");
236
237 var exceptionMessage = $"{commandName}: {errorMessage}";
238 if (!string.IsNullOrWhiteSpace(errorData))
239 {
240 exceptionMessage = $"{exceptionMessage} - {errorData}";
241 }
242
243 LogTrace("Recieved Error Response {0}: {1} {2}", modified.CommandId, message, errorData);
244 throw new CommandResponseException(exceptionMessage)
245 {
246 Code = modified.Result.Value<long>("code")
247 };
248 }
249
250 return modified.Result;
251 }
252 }
253 else
254 {
255 if (this.sessionSocket != null)
256 {
257 LogTrace("WebSocket is not connected (current state is {0}); not sending {1}", this.sessionSocket.State, message.CommandName);
258 }
259 }
260
261 return null;
262 }
263
264 /// <summary>
265 /// Disposes of the DevToolsSession and frees all resources.
266 ///</summary>
267 public void Dispose()
268 {
269 this.Dispose(true);
270 }
271
272 /// <summary>
273 /// Asynchronously starts the session.
274 /// </summary>
275 /// <param name="requestedProtocolVersion">The requested version of the protocol to use in communicating with the browswer.</param>
276 /// <returns>A task that represents the asynchronous operation.</returns>
277 internal async Task StartSession(int requestedProtocolVersion)
278 {
279 int protocolVersion = await InitializeProtocol(requestedProtocolVersion);
280 this.domains = DevToolsDomains.InitializeDomains(protocolVersion, this);
281 await this.InitializeSocketConnection();
282 await this.InitializeSession();
283 try
284 {
285 // Wrap this in a try-catch, because it's not the end of the
286 // world if clearing the log doesn't work.
287 await this.domains.Log.Clear();
288 LogTrace("Log cleared.", this.attachedTargetId);
289 }
290 catch (WebDriverException)
291 {
292 }
293 }
294
295 /// <summary>
296 /// Asynchronously stops the session.
297 /// </summary>
298 /// <param name="manualDetach"><see langword="true"/> to manually detach the session
299 /// from its attached target; otherswise <see langword="false""/>.</param>
300 /// <returns>A task that represents the asynchronous operation.</returns>
301 internal async Task StopSession(bool manualDetach)
302 {
303 if (this.attachedTargetId != null)
304 {
305 this.Domains.Target.TargetDetached -= this.OnTargetDetached;
306 string sessionId = this.ActiveSessionId;
307 this.ActiveSessionId = null;
308 if (manualDetach)
309 {
310 await this.Domains.Target.DetachFromTarget(sessionId, this.attachedTargetId);
311 }
312
313 this.attachedTargetId = null;
314 }
315 }
316
317 protected void Dispose(bool disposing)
318 {
319 if (!this.isDisposed)
320 {
321 if (disposing)
322 {
323 this.Domains.Target.TargetDetached -= this.OnTargetDetached;
324 this.pendingCommands.Clear();
325 this.TerminateSocketConnection();
326
327 // Note: Canceling the receive task will dispose of
328 // the underlying ClientWebSocket instance.
329 this.CancelReceiveTask();
330 }
331
332 this.isDisposed = true;
333 }
334 }
335
336 private async Task<int> InitializeProtocol(int requestedProtocolVersion)
337 {
338 int protocolVersion = requestedProtocolVersion;
339 if (this.websocketAddress == null)
340 {
341 string debuggerUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}", this.debuggerEndpoint);
342 string rawVersionInfo = string.Empty;
343 using (HttpClient client = new HttpClient())
344 {
345 client.BaseAddress = new Uri(debuggerUrl);
346 rawVersionInfo = await client.GetStringAsync("/json/version");
347 }
348
349 var versionInfo = JsonConvert.DeserializeObject<DevToolsVersionInfo>(rawVersionInfo);
350 this.websocketAddress = versionInfo.WebSocketDebuggerUrl;
351
352 if (requestedProtocolVersion == AutoDetectDevToolsProtocolVersion)
353 {
354 bool versionParsed = int.TryParse(versionInfo.BrowserMajorVersion, out protocolVersion);
355 if (!versionParsed)
356 {
357 throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, "Unable to parse version number received from browser. Reported browser version string is '{0}'", versionInfo.Browser));
358 }
359 }
360 }
361 else
362 {
363 if (protocolVersion == AutoDetectDevToolsProtocolVersion)
364 {
365 throw new WebDriverException("A WebSocket address for DevTools protocol has been detected, but the protocol version cannot be automatically detected. You must specify a protocol version.");
366 }
367 }
368
369 return protocolVersion;
370 }
371
372 private async Task InitializeSocketConnection()
373 {
374 LogTrace("Creating WebSocket");
375 this.sessionSocket = new ClientWebSocket();
376 this.sessionSocket.Options.KeepAliveInterval = TimeSpan.Zero;
377
378 try
379 {
380 var timeoutTokenSource = new CancellationTokenSource(this.openConnectionWaitTimeSpan);
381 await this.sessionSocket.ConnectAsync(new Uri(this.websocketAddress), timeoutTokenSource.Token);
382 while (this.sessionSocket.State != WebSocketState.Open && !timeoutTokenSource.Token.IsCancellationRequested) ;
383 }
384 catch (OperationCanceledException e)
385 {
386 throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, "Could not establish WebSocket connection within {0} seconds.", this.openConnectionWaitTimeSpan.TotalSeconds), e);
387 }
388
389 LogTrace("WebSocket created; starting message listener");
390 this.receiveCancellationToken = new CancellationTokenSource();
391 this.receiveTask = Task.Run(() => ReceiveMessage().ConfigureAwait(false));
392 }
393
394 private async Task InitializeSession()
395 {
396 LogTrace("Creating session");
397 if (this.attachedTargetId == null)
398 {
399 // Set the attached target ID to a "pending connection" value
400 // (any non-null will do, so we choose the empty string), so
401 // that when getting the available targets, we won't
402 // recursively try to call InitializeSession.
403 this.attachedTargetId = "";
404 var targets = await this.domains.Target.GetTargets();
405 foreach (var target in targets)
406 {
407 if (target.Type == "page")
408 {
409 this.attachedTargetId = target.TargetId;
410 LogTrace("Found Target ID {0}.", this.attachedTargetId);
411 break;
412 }
413 }
414 }
415
416 if (this.attachedTargetId == "")
417 {
418 this.attachedTargetId = null;
419 throw new WebDriverException("Unable to find target to attach to, no taargets of type 'page' available");
420 }
421
422 string sessionId = await this.domains.Target.AttachToTarget(this.attachedTargetId);
423 LogTrace("Target ID {0} attached. Active session ID: {1}", this.attachedTargetId, sessionId);
424 this.ActiveSessionId = sessionId;
425
426 await this.domains.Target.SetAutoAttach();
427 LogTrace("AutoAttach is set.", this.attachedTargetId);
428
429 this.domains.Target.TargetDetached += this.OnTargetDetached;
430 }
431
432 private async void OnTargetDetached(object sender, TargetDetachedEventArgs e)
433 {
434 if (e.SessionId == this.ActiveSessionId && e.TargetId == this.attachedTargetId)
435 {
436 await this.StopSession(false);
437 }
438 }
439
440 private void TerminateSocketConnection()
441 {
442 if (this.sessionSocket != null && this.sessionSocket.State == WebSocketState.Open)
443 {
444 var closeConnectionTokenSource = new CancellationTokenSource(this.closeConnectionWaitTimeSpan);
445 try
446 {
447 // Since Chromium-based DevTools does not respond to the close
448 // request with a correctly echoed WebSocket close packet, but
449 // rather just terminates the socket connection, so we have to
450 // catch the exception thrown when the socket is terminated
451 // unexpectedly. Also, because we are using async, waiting for
452 // the task to complete might throw a TaskCanceledException,
453 // which we should also catch. Additiionally, there are times
454 // when mulitple failure modes can be seen, which will throw an
455 // AggregateException, consolidating several exceptions into one,
456 // and this too must be caught. Finally, the call to CloseAsync
457 // will hang even though the connection is already severed.
458 // Wait for the task to complete for a short time (since we're
459 // restricted to localhost, the default of 2 seconds should be
460 // plenty; if not, change the initialization of the timout),
461 // and if the task is still running, then we assume the connection
462 // is properly closed.
463 LogTrace("Sending socket close request");
464 Task closeTask = Task.Run(async () => await this.sessionSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, closeConnectionTokenSource.Token));
465 closeTask.Wait();
466 }
467 catch (WebSocketException)
468 {
469 }
470 catch (TaskCanceledException)
471 {
472 }
473 catch (AggregateException)
474 {
475 }
476 }
477 }
478
479 private void CancelReceiveTask()
480 {
481 if (this.receiveTask != null)
482 {
483 // Wait for the recieve task to be completely exited (for
484 // whatever reason) before attempting to dispose it. Also
485 // note that canceling the receive task will dispose of the
486 // underlying WebSocket.
487 this.receiveCancellationToken.Cancel();
488 this.receiveTask.Wait();
489 this.receiveTask.Dispose();
490 this.receiveTask = null;
491 }
492 }
493
494 private async Task ReceiveMessage()
495 {
496 var cancellationToken = this.receiveCancellationToken.Token;
497 try
498 {
499 var buffer = WebSocket.CreateClientBuffer(1024, 1024);
500 while (this.sessionSocket.State != WebSocketState.Closed && !cancellationToken.IsCancellationRequested)
501 {
502 WebSocketReceiveResult result = await this.sessionSocket.ReceiveAsync(buffer, cancellationToken);
503 if (!cancellationToken.IsCancellationRequested)
504 {
505 if (result.MessageType == WebSocketMessageType.Close && this.sessionSocket.State == WebSocketState.CloseReceived)
506 {
507 LogTrace("Got WebSocket close message from browser");
508 await this.sessionSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken);
509 }
510 }
511
512 if (this.sessionSocket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close)
513 {
514 using (var stream = new MemoryStream())
515 {
516 stream.Write(buffer.Array, 0, result.Count);
517 while (!result.EndOfMessage)
518 {
519 result = await this.sessionSocket.ReceiveAsync(buffer, cancellationToken);
520 stream.Write(buffer.Array, 0, result.Count);
521 }
522
523 stream.Seek(0, SeekOrigin.Begin);
524 using (var reader = new StreamReader(stream, Encoding.UTF8))
525 {
526 string message = reader.ReadToEnd();
527 ProcessIncomingMessage(message);
528 }
529 }
530 }
531 }
532 }
533 catch (OperationCanceledException)
534 {
535 }
536 catch (WebSocketException)
537 {
538 }
539 finally
540 {
541 this.sessionSocket.Dispose();
542 this.sessionSocket = null;
543 }
544 }
545
546 private void ProcessIncomingMessage(string message)
547 {
548 var messageObject = JObject.Parse(message);
549
550 if (messageObject.TryGetValue("id", out var idProperty))
551 {
552 var commandId = idProperty.Value<long>();
553
554 DevToolsCommandData commandInfo;
555 if (this.pendingCommands.TryGetValue(commandId, out commandInfo))
556 {
557 if (messageObject.TryGetValue("error", out var errorProperty))
558 {
559 commandInfo.IsError = true;
560 commandInfo.Result = errorProperty;
561 }
562 else
563 {
564 commandInfo.Result = messageObject["result"];
565 LogTrace("Recieved Response {0}: {1}", commandId, commandInfo.Result.ToString());
566 }
567
568 commandInfo.SyncEvent.Set();
569 }
570 else
571 {
572 LogError("Recieved Unknown Response {0}: {1}", commandId, message);
573 }
574
575 return;
576 }
577
578 if (messageObject.TryGetValue("method", out var methodProperty))
579 {
580 var method = methodProperty.Value<string>();
581 var methodParts = method.Split(new char[] { '.' }, 2);
582 var eventData = messageObject["params"];
583
584 LogTrace("Recieved Event {0}: {1}", method, eventData.ToString());
585 OnDevToolsEventReceived(new DevToolsEventReceivedEventArgs(methodParts[0], methodParts[1], eventData));
586 return;
587 }
588
589 LogTrace("Recieved Other: {0}", message);
590 }
591
592 private void OnDevToolsEventReceived(DevToolsEventReceivedEventArgs e)
593 {
594 if (DevToolsEventReceived != null)
595 {
596 DevToolsEventReceived(this, e);
597 }
598 }
599
600 private void LogTrace(string message, params object[] args)
601 {
602 if (LogMessage != null)
603 {
604 LogMessage(this, new DevToolsSessionLogMessageEventArgs(DevToolsSessionLogLevel.Trace, message, args));
605 }
606 }
607
608 private void LogError(string message, params object[] args)
609 {
610 if (LogMessage != null)
611 {
612 LogMessage(this, new DevToolsSessionLogMessageEventArgs(DevToolsSessionLogLevel.Error, message, args));
613 }
614 }
615 }
616 }