Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions source/ChromeDevTools/ChromeSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,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
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
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
Copy Markdown
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
Copy Markdown
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
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
114 changes: 63 additions & 51 deletions source/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,72 +17,84 @@ 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
{
Width = ViewPortWidth,
Height = ViewPortHeight
});

var navigateResponse = await chromeSession.SendAsync(new NavigateCommand
// 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
{
Url = "http://www.google.com"
});
Console.WriteLine("NavigateResponse: " + navigateResponse.Id);
// STEP 3 - Create a debugging session
var sessionInfo = (await chromeProcess.GetSessionInfo()).LastOrDefault();
var chromeSessionFactory = new ChromeSessionFactory();
var chromeSession = chromeSessionFactory.Create(sessionInfo.WebSocketDebuggerUrl);

// 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();

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 SetVisibleSizeCommand {Width = ViewPortWidth, Height = height});

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 ..");
}
Expand Down