Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request error handling #23

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions source/ChromeDevTools/ChromeSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class ChromeSession : IChromeSession
private readonly ConcurrentDictionary<string, ConcurrentBag<Action<object>>> _handlers = new ConcurrentDictionary<string, ConcurrentBag<Action<object>>>();
private ICommandFactory _commandFactory;
private IEventFactory _eventFactory;
private readonly Action<Exception> _onError;
private ManualResetEvent _openEvent = new ManualResetEvent(false);
private ManualResetEvent _publishEvent = new ManualResetEvent(false);
private ConcurrentDictionary<long, ManualResetEventSlim> _requestWaitHandles = new ConcurrentDictionary<long, ManualResetEventSlim>();
Expand All @@ -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<Exception> onError)
Copy link
Member

@brewdente brewdente Dec 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onError should be optional. If it's not provided, it should throw by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is a breaking change, but if we keep the throwing behaviour, there is no place to catch it - since it can occur on another thread while recieving event from chrome.
Are you sure you really want to keep this behaviour by default instead of forcing user to handle it?

{
_endpoint = endpoint;
_commandFactory = commandFactory;
_responseFactory = responseFactory;
_eventFactory = eventFactory;
_onError = onError;
}

public void Dispose()
Expand Down Expand Up @@ -81,17 +83,17 @@ public Task<ICommandResponse> SendAsync<T>(CancellationToken cancellationToken)
return SendCommand(command, cancellationToken);
}

public Task<CommandResponse<T>> SendAsync<T>(ICommand<T> parameter, CancellationToken cancellationToken)
public Task<ICommandResponseWrapper<T>> SendAsync<T>(ICommand<T> parameter, CancellationToken cancellationToken)
{
var command = _commandFactory.Create(parameter);
var task = SendCommand(command, cancellationToken);
return CastTaskResult<ICommandResponse, CommandResponse<T>>(task);
return TransformTaskResult(task, response => (ICommandResponseWrapper<T>)new CommandResponseWrapper<T>(response));
}

private Task<TDerived> CastTaskResult<TBase, TDerived>(Task<TBase> task) where TDerived: TBase
private Task<TDerived> TransformTaskResult<TBase, TDerived>(Task<TBase> task, Func<TBase, TDerived> transform)
{
var tcs = new TaskCompletionSource<TDerived>();
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);
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion source/ChromeDevTools/ChromeSessionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace MasterDevs.ChromeDevTools
{
public static class ChromeSessionExtensions
{
public static Task<CommandResponse<T>> SendAsync<T>(this IChromeSession session, ICommand<T> parameter)
public static Task<ICommandResponseWrapper<T>> SendAsync<T>(this IChromeSession session, ICommand<T> parameter)
{
return session.SendAsync(parameter, CancellationToken.None);
}
Expand Down
12 changes: 7 additions & 5 deletions source/ChromeDevTools/ChromeSessionFactory.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
#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<Exception> onError)
{
return Create(sessionInfo.WebSocketDebuggerUrl);
return Create(sessionInfo.WebSocketDebuggerUrl, onError);
}

public IChromeSession Create(string endpointUrl)
public IChromeSession Create(string endpointUrl, Action<Exception> onError)
{
// Sometimes binding to localhost might resolve wrong AddressFamily, force IPv4
endpointUrl = endpointUrl.Replace("ws://localhost", "ws://127.0.0.1");
var methodTypeMap = new MethodTypeMap();
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;
}
}
Expand Down
49 changes: 49 additions & 0 deletions source/ChromeDevTools/CommandResponseWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;

namespace MasterDevs.ChromeDevTools
{
public interface ICommandResponseWrapper<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@svatal any reason not to merge the ICommandResponse and ICommandResponseWrapper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been afraid how deserialization would cope with a class that have something more than a basic properties. Did not tested it though.

{
long Id { get; }
string Method { get; }
bool IsError();
T Result { get; }
Error Error { get; }
}

public class CommandResponseWrapper<T> : ICommandResponseWrapper<T>
{
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<T>;
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}.")
{
}
}
}
2 changes: 1 addition & 1 deletion source/ChromeDevTools/IChromeSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public interface ICommand<T>
}
public interface IChromeSession
{
Task<CommandResponse<TResponse>> SendAsync<TResponse>(ICommand<TResponse> parameter, CancellationToken cancellationToken);
Task<ICommandResponseWrapper<TResponse>> SendAsync<TResponse>(ICommand<TResponse> parameter, CancellationToken cancellationToken);

Task<ICommandResponse> SendAsync<T>(CancellationToken cancellationToken);

Expand Down
6 changes: 4 additions & 2 deletions source/ChromeDevTools/IChromeSessionFactory.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace MasterDevs.ChromeDevTools
using System;

namespace MasterDevs.ChromeDevTools
{
public interface IChromeSessionFactory
{
IChromeSession Create(string endpointUrl);
IChromeSession Create(string endpointUrl, Action<Exception> onError);
}
}
1 change: 1 addition & 0 deletions source/ChromeDevTools/MasterDevs.ChromeDevTools.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ChromeProcessFactory.cs" />
<Compile Include="CommandResponseWrapper.cs" />
<Compile Include="IDirectoryCleaner.cs" />
<Compile Include="LocalChromeProcess.cs" />
<Compile Include="ChromeSession.cs" />
Expand Down
120 changes: 69 additions & 51 deletions source/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,94 @@ 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 SetVisibleSizeCommand
// 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
});
// 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<Protocol.Chrome.Page.EnableCommand>();
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 SetVisibleSizeCommand
{
Width = ViewPortWidth,
Height = ViewPortHeight
});

chromeSession.Subscribe<LoadEventFiredEvent>(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<Protocol.Chrome.Page.EnableCommand>();
Console.WriteLine($"PageEnable: {pageEnableResult.Id}");

await chromeSession.SendAsync(new SetVisibleSizeCommand {Width = ViewPortWidth, Height = height});
// 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<LoadEventFiredEvent>(ev =>
{
Console.WriteLine($"LoadEventFiredEvent: {ev.Timestamp}");
loadEventFired.Set();
});
loadEventFired.Wait();

// The page is ready in the browser, now we can take the screenshot

Console.WriteLine("Taking screenshot");
var screenshot = await chromeSession.SendAsync(new CaptureScreenshotCommand {Format = "png"});
// 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;

var data = Convert.FromBase64String(screenshot.Result.Data);
File.WriteAllBytes("output.png", data);
Console.WriteLine("Screenshot stored");
await chromeSession.SendAsync(
new SetVisibleSizeCommand {Width = ViewPortWidth, Height = height});

// tell the main thread we are done
screenshotDone.Set();
});
});
Console.WriteLine("Taking screenshot");
var screenshot = await chromeSession.SendAsync(new CaptureScreenshotCommand {Format = "png"});

// wait for screenshoting thread to (start and) finish
screenshotDone.Wait();
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);
}
}
}