diff --git a/source/ChromeDevTools/ChromeSession.cs b/source/ChromeDevTools/ChromeSession.cs index c8d6971f..c1e24368 100644 --- a/source/ChromeDevTools/ChromeSession.cs +++ b/source/ChromeDevTools/ChromeSession.cs @@ -16,6 +16,7 @@ public class ChromeSession : IChromeSession private readonly ConcurrentDictionary>> _handlers = new ConcurrentDictionary>>(); private ICommandFactory _commandFactory; private IEventFactory _eventFactory; + private readonly Action _onError; private ManualResetEvent _openEvent = new ManualResetEvent(false); private ManualResetEvent _publishEvent = new ManualResetEvent(false); private ConcurrentDictionary _requestWaitHandles = new ConcurrentDictionary(); @@ -24,12 +25,13 @@ public class ChromeSession : IChromeSession private WebSocket _webSocket; private static object _Lock = new object(); - public ChromeSession(string endpoint, ICommandFactory commandFactory, ICommandResponseFactory responseFactory, IEventFactory eventFactory) + public ChromeSession(string endpoint, ICommandFactory commandFactory, ICommandResponseFactory responseFactory, IEventFactory eventFactory, Action onError) { _endpoint = endpoint; _commandFactory = commandFactory; _responseFactory = responseFactory; _eventFactory = eventFactory; + _onError = onError; } public void Dispose() @@ -81,17 +83,17 @@ public Task SendAsync(CancellationToken cancellationToken) return SendCommand(command, cancellationToken); } - public Task> SendAsync(ICommand parameter, CancellationToken cancellationToken) + public Task> SendAsync(ICommand parameter, CancellationToken cancellationToken) { var command = _commandFactory.Create(parameter); var task = SendCommand(command, cancellationToken); - return CastTaskResult>(task); + return TransformTaskResult(task, response => (ICommandResponseWrapper)new CommandResponseWrapper(response)); } - private Task CastTaskResult(Task task) where TDerived: TBase + private Task TransformTaskResult(Task task, Func transform) { var tcs = new TaskCompletionSource(); - task.ContinueWith(t => tcs.SetResult((TDerived)t.Result), + task.ContinueWith(t => tcs.SetResult(transform(t.Result)), TaskContinuationOptions.OnlyOnRanToCompletion); task.ContinueWith(t => tcs.SetException(t.Exception.InnerExceptions), TaskContinuationOptions.OnlyOnFaulted); @@ -235,12 +237,12 @@ private void WebSocket_DataReceived(object sender, DataReceivedEventArgs e) HandleEvent(evnt); return; } - throw new Exception("Don't know what to do with response: " + e.Data); + _onError(new Exception("Don't know what to do with response: " + e.Data)); } private void WebSocket_Error(object sender, SuperSocket.ClientEngine.ErrorEventArgs e) { - throw e.Exception; + _onError(e.Exception); } private void WebSocket_MessageReceived(object sender, MessageReceivedEventArgs e) @@ -257,7 +259,7 @@ private void WebSocket_MessageReceived(object sender, MessageReceivedEventArgs e HandleEvent(evnt); return; } - throw new Exception("Don't know what to do with response: " + e.Message); + _onError(new Exception("Don't know what to do with response: " + e.Message)); } private void WebSocket_Opened(object sender, EventArgs e) diff --git a/source/ChromeDevTools/ChromeSessionExtensions.cs b/source/ChromeDevTools/ChromeSessionExtensions.cs index 667890c0..349d5c02 100644 --- a/source/ChromeDevTools/ChromeSessionExtensions.cs +++ b/source/ChromeDevTools/ChromeSessionExtensions.cs @@ -5,7 +5,7 @@ namespace MasterDevs.ChromeDevTools { public static class ChromeSessionExtensions { - public static Task> SendAsync(this IChromeSession session, ICommand parameter) + public static Task> SendAsync(this IChromeSession session, ICommand parameter) { return session.SendAsync(parameter, CancellationToken.None); } diff --git a/source/ChromeDevTools/ChromeSessionFactory.cs b/source/ChromeDevTools/ChromeSessionFactory.cs index 36facc27..32d71770 100644 --- a/source/ChromeDevTools/ChromeSessionFactory.cs +++ b/source/ChromeDevTools/ChromeSessionFactory.cs @@ -1,14 +1,16 @@ -#if !NETSTANDARD1_5 +using System; + +#if !NETSTANDARD1_5 namespace MasterDevs.ChromeDevTools { public class ChromeSessionFactory : IChromeSessionFactory { - public IChromeSession Create(ChromeSessionInfo sessionInfo) + public IChromeSession Create(ChromeSessionInfo sessionInfo, Action onError) { - return Create(sessionInfo.WebSocketDebuggerUrl); + return Create(sessionInfo.WebSocketDebuggerUrl, onError); } - public IChromeSession Create(string endpointUrl) + public IChromeSession Create(string endpointUrl, Action onError) { // Sometimes binding to localhost might resolve wrong AddressFamily, force IPv4 endpointUrl = endpointUrl.Replace("ws://localhost", "ws://127.0.0.1"); @@ -16,7 +18,7 @@ public IChromeSession Create(string endpointUrl) var commandFactory = new CommandFactory(); var responseFactory = new CommandResponseFactory(methodTypeMap, commandFactory); var eventFactory = new EventFactory(methodTypeMap); - var session = new ChromeSession(endpointUrl, commandFactory, responseFactory, eventFactory); + var session = new ChromeSession(endpointUrl, commandFactory, responseFactory, eventFactory, onError); return session; } } diff --git a/source/ChromeDevTools/CommandResponseWrapper.cs b/source/ChromeDevTools/CommandResponseWrapper.cs new file mode 100644 index 00000000..830574c0 --- /dev/null +++ b/source/ChromeDevTools/CommandResponseWrapper.cs @@ -0,0 +1,49 @@ +using System; + +namespace MasterDevs.ChromeDevTools +{ + public interface ICommandResponseWrapper + { + long Id { get; } + string Method { get; } + bool IsError(); + T Result { get; } + Error Error { get; } + } + + public class CommandResponseWrapper : ICommandResponseWrapper + { + private readonly ICommandResponse _response; + + public CommandResponseWrapper(ICommandResponse response) + { + _response = response; + } + + public long Id => _response.Id; + public string Method => _response.Method; + + public bool IsError() => _response is IErrorResponse; + + public T Result + { + get + { + var commandResponse = _response as CommandResponse; + if (commandResponse != null) + return commandResponse.Result; + throw new ResultNotAvailableException((IErrorResponse)_response, typeof(T)); + } + } + + public Error Error => (_response as IErrorResponse)?.Error; + } + + public class ResultNotAvailableException : Exception + { + public ResultNotAvailableException(IErrorResponse response, Type type) + : base($"Unhandled command error {{ Code: {response.Error.Code}, Message: {response.Error.Message} }} to command {response.Id} requesting {type}.") + { + } + } +} \ No newline at end of file diff --git a/source/ChromeDevTools/IChromeSession.cs b/source/ChromeDevTools/IChromeSession.cs index ef5d88e8..06b1dd16 100644 --- a/source/ChromeDevTools/IChromeSession.cs +++ b/source/ChromeDevTools/IChromeSession.cs @@ -10,7 +10,7 @@ public interface ICommand } public interface IChromeSession { - Task> SendAsync(ICommand parameter, CancellationToken cancellationToken); + Task> SendAsync(ICommand parameter, CancellationToken cancellationToken); Task SendAsync(CancellationToken cancellationToken); diff --git a/source/ChromeDevTools/IChromeSessionFactory.cs b/source/ChromeDevTools/IChromeSessionFactory.cs index 286019d7..12ce7749 100644 --- a/source/ChromeDevTools/IChromeSessionFactory.cs +++ b/source/ChromeDevTools/IChromeSessionFactory.cs @@ -1,7 +1,9 @@ -namespace MasterDevs.ChromeDevTools +using System; + +namespace MasterDevs.ChromeDevTools { public interface IChromeSessionFactory { - IChromeSession Create(string endpointUrl); + IChromeSession Create(string endpointUrl, Action onError); } } \ No newline at end of file diff --git a/source/ChromeDevTools/MasterDevs.ChromeDevTools.csproj b/source/ChromeDevTools/MasterDevs.ChromeDevTools.csproj index edd8226b..bf2340ae 100644 --- a/source/ChromeDevTools/MasterDevs.ChromeDevTools.csproj +++ b/source/ChromeDevTools/MasterDevs.ChromeDevTools.csproj @@ -60,6 +60,7 @@ + diff --git a/source/Sample/Program.cs b/source/Sample/Program.cs index fcb1eea0..5582b167 100644 --- a/source/Sample/Program.cs +++ b/source/Sample/Program.cs @@ -16,82 +16,99 @@ private static void Main(string[] args) { Task.Run(async () => { - // synchronization - var screenshotDone = new ManualResetEventSlim(); - // STEP 1 - Run Chrome var chromeProcessFactory = new ChromeProcessFactory(new StubbornDirectoryCleaner()); using (var chromeProcess = chromeProcessFactory.Create(9222, true)) { - // STEP 2 - Create a debugging session - var sessionInfo = (await chromeProcess.GetSessionInfo()).LastOrDefault(); - var chromeSessionFactory = new ChromeSessionFactory(); - var chromeSession = chromeSessionFactory.Create(sessionInfo.WebSocketDebuggerUrl); - - // STEP 3 - Send a command + // STEP 2 - Handle communication errors // - // Here we are sending a commands to tell chrome to set the viewport size - // and navigate to the specified URL - await chromeSession.SendAsync(new SetDeviceMetricsOverrideCommand + // There are two ways how to handle communication errors: + // 1) check .IsError() for every command before accessing the .Result + // 2) access the .Result directly and handle (or don't handle ..) the exception + // We are using here the second option + try { - Width = ViewPortWidth, - Height = ViewPortHeight, - Scale = 1 - }); + // STEP 3 - Create a debugging session + var sessionInfo = (await chromeProcess.GetSessionInfo()).LastOrDefault(); + var chromeSessionFactory = new ChromeSessionFactory(); + var chromeSession = chromeSessionFactory.Create(sessionInfo.WebSocketDebuggerUrl,OnError ); - var navigateResponse = await chromeSession.SendAsync(new NavigateCommand - { - Url = "http://www.google.com" - }); - Console.WriteLine("NavigateResponse: " + navigateResponse.Id); - - // STEP 4 - Register for events (in this case, "Page" domain events) - // send an command to tell chrome to send us all Page events - // but we only subscribe to certain events in this session - var pageEnableResult = await chromeSession.SendAsync(); - Console.WriteLine("PageEnable: " + pageEnableResult.Id); + // STEP 4 - Send a command + // + // Here we are sending a commands to tell chrome to set the viewport size + // and navigate to the specified URL + await chromeSession.SendAsync(new SetDeviceMetricsOverrideCommand + { + Width = ViewPortWidth, + Height = ViewPortHeight, + Scale = 1 + }); - chromeSession.Subscribe(loadEventFired => - { - // we cannot block in event handler, hence the task - Task.Run(async () => + var navigateResponse = await chromeSession.SendAsync(new NavigateCommand { - Console.WriteLine("LoadEventFiredEvent: " + loadEventFired.Timestamp); + Url = "http://www.google.com" + }); + Console.WriteLine($"NavigateResponse: {navigateResponse.Id}"); - var documentNodeId = (await chromeSession.SendAsync(new GetDocumentCommand())).Result.Root.NodeId; - var bodyNodeId = - (await chromeSession.SendAsync(new QuerySelectorCommand - { - NodeId = documentNodeId, - Selector = "body" - })).Result.NodeId; - var height = (await chromeSession.SendAsync(new GetBoxModelCommand {NodeId = bodyNodeId})).Result.Model.Height; + // STEP 5 - Register for events (in this case, "Page" domain events) + // + // send an command to tell chrome to send us all Page events + // but we only subscribe to certain events in this session + var pageEnableResult = await chromeSession.SendAsync(); + Console.WriteLine($"PageEnable: {pageEnableResult.Id}"); - await chromeSession.SendAsync(new SetDeviceMetricsOverrideCommand - { - Width = ViewPortWidth, - Height = height, - Scale = 1 - }); + // We cannot do other requests in event handler, therefore we only wait for the event to be triggered + // and continue in the main program flow + var loadEventFired = new ManualResetEventSlim(); + chromeSession.Subscribe(ev => + { + Console.WriteLine($"LoadEventFiredEvent: {ev.Timestamp}"); + loadEventFired.Set(); + }); + loadEventFired.Wait(); - Console.WriteLine("Taking screenshot"); - var screenshot = await chromeSession.SendAsync(new CaptureScreenshotCommand {Format = "png"}); + // The page is ready in the browser, now we can take the screenshot - var data = Convert.FromBase64String(screenshot.Result.Data); - File.WriteAllBytes("output.png", data); - Console.WriteLine("Screenshot stored"); + // update the VisibleSize to include whole page (extending height) + var documentNodeId = (await chromeSession.SendAsync(new GetDocumentCommand())) + .Result.Root.NodeId; + var bodyNodeId = + (await chromeSession.SendAsync(new QuerySelectorCommand + { + NodeId = documentNodeId, + Selector = "body" + })).Result.NodeId; + var height = (await chromeSession.SendAsync(new GetBoxModelCommand {NodeId = bodyNodeId})) + .Result.Model.Height; - // tell the main thread we are done - screenshotDone.Set(); + await chromeSession.SendAsync(new SetDeviceMetricsOverrideCommand + { + Width = ViewPortWidth, + Height = height, + Scale = 1 }); - }); - // wait for screenshoting thread to (start and) finish - screenshotDone.Wait(); + Console.WriteLine("Taking screenshot"); + var screenshot = await chromeSession.SendAsync(new CaptureScreenshotCommand {Format = "png"}); + + var data = Convert.FromBase64String(screenshot.Result.Data); + File.WriteAllBytes("output.png", data); + Console.WriteLine("Screenshot stored"); + } + catch (ResultNotAvailableException ex) + { + Console.WriteLine($"Error while taking screenshot: {ex.Message}"); + } Console.WriteLine("Exiting .."); } }).Wait(); } + + private static void OnError(Exception exception) + { + Console.WriteLine("Error during communication:"); + Console.WriteLine(exception); + } } } \ No newline at end of file