Skip to content
Snippets Groups Projects
Commit 51679b6c authored by brewdente's avatar brewdente
Browse files

Add the first stub of the AutoWebPerf. It works and it's in a good place

right now, with more work ahead.  This is just a checkpoint.
parent 93311e70
No related branches found
No related tags found
No related merge requests found
Showing
with 801 additions and 0 deletions
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Network
{
[MethodName(MethodName.Network.RequestServedFromCache)]
public class RequestServedFromCacheResponse : IResponse
{
public string method { get; set; }
public RequestServedFromCacheResponseParams Params { get; set; }
}
public class RequestServedFromCacheResponseParams
{
public string requestId { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Network
{
[MethodName(MethodName.Network.RequestWillBeSent)]
public class RequestWillBeSentResponse : IResponse
{
public string method { get; set; }
public RequestWillBeSentResponseParams Params { get; set; }
}
public class RequestWillBeSentResponseParams
{
public string requestId { get; set; }
public string frameId { get; set; }
public string loaderId { get; set; }
public string documentURL { get; set; }
public Request request { get; set; }
public float timestamp { get; set; }
public Initiator initiator { get; set; }
public string type { get; set; }
}
public class Request
{
public string url { get; set; }
public string method { get; set; }
public RequestHeaders headers { get; set; }
}
public class RequestHeaders
{
public string Accept { get; set; }
public string UserAgent { get; set; }
}
public class Initiator
{
public string type { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Network
{
[MethodName(MethodName.Network.ResponseReceived)]
public class ResponseReceivedResponse : IResponse
{
public string method { get; set; }
public ResponseReceivedResponseParams Params { get; set; }
}
public class ResponseReceivedResponseParams
{
public string requestId { get; set; }
public string frameId { get; set; }
public string loaderId { get; set; }
public float timestamp { get; set; }
public string type { get; set; }
public Response response { get; set; }
}
public class Response
{
public string url { get; set; }
public int status { get; set; }
public string statusText { get; set; }
public ResponseHeaders headers { get; set; }
public string mimeType { get; set; }
public bool connectionReused { get; set; }
public int connectionId { get; set; }
public int encodedDataLength { get; set; }
public bool fromDiskCache { get; set; }
public bool fromServiceWorker { get; set; }
public Timing timing { get; set; }
public string headersText { get; set; }
public Requestheaders requestHeaders { get; set; }
public string requestHeadersText { get; set; }
public string remoteIPAddress { get; set; }
public int remotePort { get; set; }
public string protocol { get; set; }
}
public class ResponseHeaders
{
public string Date { get; set; }
public string ContentEncoding { get; set; }
public string Server { get; set; }
public string XAspNetVersion { get; set; }
public string XPoweredBy { get; set; }
public string Vary { get; set; }
public string ContentType { get; set; }
public string accesscontrolalloworigin { get; set; }
public string CacheControl { get; set; }
public string SetCookie { get; set; }
public string accesscontrolallowheaders { get; set; }
public string ContentLength { get; set; }
public string Expires { get; set; }
}
public class Timing
{
public float requestTime { get; set; }
public int proxyStart { get; set; }
public int proxyEnd { get; set; }
public int dnsStart { get; set; }
public int dnsEnd { get; set; }
public int connectStart { get; set; }
public int connectEnd { get; set; }
public int sslStart { get; set; }
public int sslEnd { get; set; }
public int serviceWorkerFetchStart { get; set; }
public int serviceWorkerFetchReady { get; set; }
public int serviceWorkerFetchEnd { get; set; }
public float sendStart { get; set; }
public float sendEnd { get; set; }
public float receiveHeadersEnd { get; set; }
}
public class Requestheaders
{
public string Accept { get; set; }
public string Connection { get; set; }
public string AcceptEncoding { get; set; }
public string Host { get; set; }
public string AcceptLanguage { get; set; }
public string UserAgent { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Page
{
[MethodName("Page.navigate")]
class NavigateRequest : Request<NavigateRequestParams>
{
public NavigateRequest(long id, NavigateRequestParams param)
: base(id, param)
{
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Page
{
class NavigateRequestParams
{
public string Url { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Page
{
[MethodName(MethodName.Page.Navigate)]
class NavigateResponse : IdResponse<NavigateResponseResult>
{
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages.Page
{
class NavigateResponseResult
{
public string FrameId { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages
{
public class Request : IRequest
{
public Request(long id)
{
Id = id;
Method = LookupMethodAttribute();
}
private string LookupMethodAttribute()
{
var methodNameAttribute = this.GetType().GetCustomAttributes(typeof(MethodNameAttribute), true)
.FirstOrDefault() as MethodNameAttribute;
if(null == methodNameAttribute)
{
throw new Exception("Request(id) constructor is called on an object without a [MethodName] attribute");
}
return methodNameAttribute.MethodName;
}
public Request(long id, string method)
{
Id = id;
Method = method;
}
public long Id
{
get;
private set;
}
public string Method
{
get;
private set;
}
}
public class Request<T> : Request, IRequest<T>
{
public Request(long id, T methodParams)
: base(id)
{
Params = methodParams;
}
public T Params
{
get;
private set;
}
}
}
using AutoWebPerf.Chrome.Messages.Page;
using System;
using System.Collections.Concurrent;
using System.Threading;
namespace AutoWebPerf.Chrome.Messages
{
internal class RequestFactory : IRequestFactory
{
private readonly ConcurrentDictionary<long, string> _methods = new ConcurrentDictionary<long, string>();
private long _count = 0;
public NavigateRequest CreateNavigateRequest(string url)
{
return CreateRequest(id =>
{
var requestParams = new NavigateRequestParams
{
Url = url
};
return new NavigateRequest(id, requestParams);
});
}
public Network.EnableRequest CreateNetworkEnableRequest()
{
return CreateRequest(id => new Network.EnableRequest(id));
}
public string GetMethod(long id)
{
return _methods[id];
}
private T CreateRequest<T>(Func<long, T> factory) where T : IRequest
{
var requestId = GetNextRequestId();
var request = factory(requestId);
_methods.AddOrUpdate(request.Id, request.Method, (id, m) => request.Method);
return request;
}
private long GetNextRequestId()
{
return Interlocked.Increment(ref _count);
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages
{
class Response : IResponse
{
}
class Response<T> : IResponse<T>
{
public T Result
{
get;
set;
}
}
}
using AutoWebPerf.Chrome.Messages.Network;
using AutoWebPerf.Chrome.Messages.Page;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages
{
class ResponseFactory : IResponseFactory
{
private readonly static Lazy<Dictionary<string, Type>> _methodTypeMapLazy = new Lazy<Dictionary<string, Type>>(LoadMethodTypeMap);
private static Dictionary<string, Type> LoadMethodTypeMap()
{
var iresponseType = typeof(IResponse);
var assembly = Assembly.GetExecutingAssembly();
var assemblyTypes = assembly.GetTypes();
var responseTypes = assemblyTypes.Where(t => iresponseType.IsAssignableFrom(t) && t.IsClass);
Dictionary<string, Type> result = new Dictionary<string, Type>();
foreach(var responseType in responseTypes)
{
var methodNameAttribute = responseType.GetCustomAttribute<MethodNameAttribute>(false) as MethodNameAttribute;
if (null == methodNameAttribute) continue;
result[methodNameAttribute.MethodName] = responseType;
}
return result;
}
private readonly IRequestFactory _requestFactory;
public ResponseFactory(IRequestFactory requestFactory)
{
_requestFactory = requestFactory;
}
public IResponse Create(string responseText)
{
var jObject = JObject.Parse(responseText);
if(null != jObject["error"])
{
return jObject.ToObject<ErrorResponse>();
}
var response = ResponseFromMethodResponse(jObject);
if (null != response) return response;
response = ResponseFromMethodLookupResponse(jObject);
if (null != response) return response;
throw new Exception("We could not create a response from text: [" + responseText + "]");
}
private IResponse ResponseFromMethodLookupResponse(JObject jObject)
{
var resultObj = jObject["result"];
if (null == resultObj) return null;
var resultIdObj = jObject["id"];
if (null == resultIdObj) return null;
long resultId;
if (!long.TryParse(resultIdObj.ToString(), out resultId)) return null;
var methodLookupResult = _requestFactory.GetMethod(resultId);
if(String.IsNullOrEmpty(methodLookupResult)) return null;
return ResponseFromMethodResponse(methodLookupResult, jObject);
}
private IResponse ResponseFromMethodResponse(JObject jObject)
{
var method = jObject["method"];
if (null == method) return null;
return ResponseFromMethodResponse(method.ToString(), jObject);
}
private IResponse ResponseFromMethodResponse(string methodName, JObject jObject)
{
if(String.IsNullOrEmpty(methodName)) return null;
Type responseType = null;
if (!_methodTypeMapLazy.Value.TryGetValue(methodName, out responseType))
{
throw new ArgumentException("We could not find a response type for the method " + methodName);
}
var response = jObject.ToObject(responseType) as IResponse;
return response;
}
public IResponse Create(byte[] responseBytes)
{
var responseString = Encoding.Default.GetString(responseBytes);
return Create(responseString);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf.Chrome.Messages
{
delegate void ResponseHandler(object sender, IResponse response);
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf
{
class ChromeProcessInfo
{
public DirectoryInfo ChromeUserDirectory { get; set; }
public Process ChromeProcess { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using System.Net.WebSockets;
using System.Threading;
using WebSocket4Net;
namespace AutoWebPerf
{
/// <summary>
/// ABPS: https://markcz.wordpress.com/2012/02/18/automating-chrome-browser-from-csharp/
/// </summary>
public class ChromeWrapper
{
const string JsonPostfix = "/json";
string remoteDebuggingUri;
string sessionWSEndpoint;
public ChromeWrapper(string remoteDebuggingUri)
{
this.remoteDebuggingUri = remoteDebuggingUri;
}
public TRes SendRequest<TRes>()
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(remoteDebuggingUri + JsonPostfix);
var resp = req.GetResponse();
var respStream = resp.GetResponseStream();
StreamReader sr = new StreamReader(respStream);
var s = sr.ReadToEnd();
resp.Dispose();
return Deserialise<TRes>(s);
}
public List<RemoteSessionsResponse> GetAvailableSessions()
{
var res = this.SendRequest<List<RemoteSessionsResponse>>();
return (from r in res
where r.devtoolsFrontendUrl != null
select r).ToList();
}
public string NavigateTo(string uri)
{
// Page.navigate is working from M18
var json = @"{""method"":""Page.navigate"",""params"":{""url"":""" + uri + @"""},""id"":1}";
// Instead of Page.navigate, we can use document.location
//var json = @"{""method"":""Runtime.evaluate"",""params"":{""expression"":""document.location='" + uri + @"'"",""objectGroup"":""console"",""includeCommandLineAPI"":true,""doNotPauseOnExceptions"":false,""returnByValue"":false},""id"":1}";
return this.SendCommand(json);
}
public string GetElementsByTagName(string tagName)
{
// Page.navigate is working from M18
//var json = @"{""method"":""Page.navigate"",""params"":{""url"":""http://www.seznam.cz""},""id"":1}";
// Instead of Page.navigate, we can use document.location
var json = @"{""method"":""Runtime.evaluate"",""params"":{""expression"":""document.getElementsByTagName('" + tagName + @"')"",""objectGroup"":""console"",""includeCommandLineAPI"":true,""doNotPauseOnExceptions"":false,""returnByValue"":false},""id"":1}";
return this.SendCommand(json);
}
public string Eval(string cmd)
{
var json = @"{""method"":""Runtime.evaluate"",""params"":{""expression"":""" + cmd + @""",""objectGroup"":""console"",""includeCommandLineAPI"":true,""doNotPauseOnExceptions"":false,""returnByValue"":false},""id"":1}";
return this.SendCommand(json);
}
public string SendCommand(string cmd)
{
WebSocket4Net.WebSocket j = new WebSocket4Net.WebSocket(this.sessionWSEndpoint);
ManualResetEvent waitEvent = new ManualResetEvent(false);
ManualResetEvent closedEvent = new ManualResetEvent(false);
string message = "";
byte[] data;
Exception exc = null;
j.Opened += delegate(System.Object o, EventArgs e)
{
j.Send(cmd);
};
j.MessageReceived += delegate(System.Object o, MessageReceivedEventArgs e)
{
message = e.Message;
waitEvent.Set();
};
j.Error += delegate(System.Object o, SuperSocket.ClientEngine.ErrorEventArgs e)
{
exc = e.Exception;
waitEvent.Set();
};
j.Closed += delegate(System.Object o, EventArgs e)
{
closedEvent.Set();
};
j.DataReceived += delegate(System.Object o, DataReceivedEventArgs e)
{
data = e.Data;
waitEvent.Set();
};
j.Open();
waitEvent.WaitOne();
if (j.State == WebSocket4Net.WebSocketState.Open)
{
j.Close();
closedEvent.WaitOne();
}
if (exc != null)
throw exc;
return message;
}
private T Deserialise<T>(string json)
{
T obj = Activator.CreateInstance<T>();
using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
obj = (T)serializer.ReadObject(ms);
return obj;
}
}
private T Deserialise<T>(Stream json)
{
T obj = Activator.CreateInstance<T>();
DataContractJsonSerializer serializer = new DataContractJsonSerializer(obj.GetType());
obj = (T)serializer.ReadObject(json);
return obj;
}
public void SetActiveSession(string sessionWSEndpoint)
{
// Sometimes binding to localhost might resolve wrong AddressFamily, force IPv4
this.sessionWSEndpoint = sessionWSEndpoint.Replace("ws://localhost", "ws://127.0.0.1");
}
}
}
\ No newline at end of file
using AutoWebPerf.Chrome;
using AutoWebPerf.Chrome.Messages;
using AutoWebPerf.Chrome.Messages.Network;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WebSocket4Net;
namespace AutoWebPerf
{
class Program
{
static void Main(string[] args)
{
IChromeProxy _chromeProxy = null;
ChromeProcessInfo chromeProcessInfo = null;
try
{
chromeProcessInfo = StartChrome();
var c = new ChromeWrapper("http://localhost:" + ChromeRemoteDebuggingPort);
var sessions = c.GetAvailableSessions();
foreach (var s in sessions)
Console.WriteLine(s.url);
if (sessions.Count == 0)
throw new Exception("All debugging sessions are taken.");
// Will drive first tab session
var sessionWSEndpoint = sessions.Last().webSocketDebuggerUrl; // new tab window
c.SetActiveSession(sessionWSEndpoint);
c.NavigateTo("http://www.google.com");
var requestFactory = new RequestFactory();
_chromeProxy = new ChromeProxy(sessionWSEndpoint, new ResponseFactory(requestFactory));
_chromeProxy.Init().Wait();
var result = _chromeProxy.PublishAsync(requestFactory.CreateNetworkEnableRequest()).Result;
_chromeProxy.Subscribe<RequestWillBeSentResponse>((o, e) =>
{
Console.WriteLine("We got it!");
});
_chromeProxy.PublishAsync(requestFactory.CreateNavigateRequest("http://www.google.com")).Wait();
Console.ReadLine();
}
finally
{
if(null != _chromeProxy)
{
_chromeProxy.Dispose();
}
KillChrome(chromeProcessInfo);
}
}
private static void KillChrome(ChromeProcessInfo chromeProcessInfo)
{
try
{
chromeProcessInfo.ChromeProcess.Kill();
} catch
{
// right now, the process has been killed ... so i'm swallowing this during setup/testing
}
try
{
chromeProcessInfo.ChromeUserDirectory.Delete(true);
} catch
{
Thread.Sleep(500);
chromeProcessInfo.ChromeUserDirectory.Delete(true);
}
}
private static ChromeProcessInfo StartChrome()
{
string path = Path.GetRandomFileName();
var directoryInfo = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), path));
var remoteDebuggingArg = "--remote-debugging-port=" + ChromeRemoteDebuggingPort;
var userDirectoryArg = "--user-data-dir=\"" + directoryInfo.FullName + "\"";
var chromeProcessArgs = remoteDebuggingArg + " " + userDirectoryArg + " --bwsi --no-first-run";
var processStartInfo = new ProcessStartInfo(@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", chromeProcessArgs);
var chromeProcess = Process.Start(processStartInfo);
return new ChromeProcessInfo
{
ChromeProcess = chromeProcess,
ChromeUserDirectory = directoryInfo
};
}
public const int ChromeRemoteDebuggingPort = 9222;
}
}
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("AutoWebPerf")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AutoWebPerf")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("d4a99e75-3de5-4de5-8bb9-00659c480504")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace AutoWebPerf
{
[Serializable]
[DataContract]
public class RemoteSessionsResponse
{
public RemoteSessionsResponse() { }
[DataMember]
public string devtoolsFrontendUrl;
[DataMember]
public string faviconUrl;
[DataMember]
public string thumbnailUrl;
[DataMember]
public string title;
[DataMember]
public string url;
[DataMember]
public string webSocketDebuggerUrl;
}
}
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="6.0.8" targetFramework="net45" />
<package id="WebSocket4Net" version="0.12" targetFramework="net45" />
</packages>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment