Chromium.cs

using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Runtime.Serialization.Json;
using System.Text;
using Websocket.Client;
 
namespace GenXdev.Helpers
{
    public class Chromium
    {
        public static Int64 Id = 0;
        public static object padLock = new object();
 
        const string JsonPostfix = "/json";
 
        public string remoteDebuggingUri;
        public string sessionWSEndpoint;
 
        public int Port { get; private set; }
        public Chromium(string remoteDebuggingUri)
        {
            this.remoteDebuggingUri = remoteDebuggingUri;
            this.Port = (new Uri(this.remoteDebuggingUri)).Port;
        }
 
        public TRes SendRequest<TRes>()
        {
            using (HttpClient client = new HttpClient())
            {
                return (Task.Run(async () =>
                {
                    string url = remoteDebuggingUri + JsonPostfix;
                    var response = await client.GetStringAsync(url);
                    return Deserialise<TRes>(response);
                }).Result);
            }
        }
 
        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"":""http://www.seznam.cz""},""id"":1}";
 
            // Instead of Page.navigate, we can use document.location
            var json = @"{""method"":""Runtime.evaluate"",""params"":{""expression"":" + Serialization.ToJson("document.location='" + uri + "'") + @",""objectGroup"":""console"",""allowUnsafeEvalBlockedByCSP"":true,""includeCommandLineAPI"":true,""doNotPauseOnExceptions"":false,""returnByValue"":false},""id"":" + (Interlocked.Increment(ref Chromium.Id)) + "}";
            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"",""allowUnsafeEvalBlockedByCSP"":true,""includeCommandLineAPI"":true,""doNotPauseOnExceptions"":false,""returnByValue"":false},""id"":" + (Interlocked.Increment(ref Chromium.Id)) + "}";
            return this.SendCommand(json);
        }
 
 
        public string Eval(string cmd)
        {
            return Eval(cmd, false);
        }
        public string GetEvalCommand(string cmd, bool allowTopLevelAwait = false)
        {
            string awaitParam = "";
            if (allowTopLevelAwait)
            {
                awaitParam = @"""replMode"":true,";
            }
            // /* @"""objectGroup"":""console"",*/
            var json = @"{""method"":""Runtime.evaluate"",""params"":{""expression"":" + Serialization.ToJson(cmd) + @"," + awaitParam + @"""allowUnsafeEvalBlockedByCSP"":true,""includeCommandLineAPI"":true,""doNotPauseOnExceptions"":true,""returnByValue"":true,""awaitPromise"":true},""id"":" + (Interlocked.Increment(ref Chromium.Id)) + "}";
 
            // return json;
            return json;
        }
 
        public string Eval(string cmd, bool allowTopLevelAwait = false, uint timeoutseconds = 30)
        {
            // return json;
            return this.SendCommand(GetEvalCommand(cmd, allowTopLevelAwait), timeoutseconds);
        }
 
        public static ConcurrentDictionary<string, WebsocketClient> SocketCache = new ConcurrentDictionary<string, WebsocketClient>();
        public static ConcurrentDictionary<string, ConcurrentQueue<string>> SocketCacheData = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
 
        public string SendCommand(string cmd, uint timeoutseconds = 30)
        {
            // // Console.WriteLine("SendCommand: timeout: " + timeoutseconds.ToString() + " seconds");
 
            var id = Interlocked.Read(ref Chromium.Id);
            var url = new Uri(this.sessionWSEndpoint);
            var padlock = new object();
            System.DateTime lastRx = System.DateTime.UtcNow;
 
            WebsocketClient client = null;
            if (SocketCache.ContainsKey(url.ToString()))
            {
                client = SocketCache[url.ToString()];
                if (!client.IsStarted)
                {
 
                    client.Start();
                }
            }
            else
            {
                var factory = new Func<ClientWebSocket>(() => new ClientWebSocket
                {
                    Options = {
 
                        KeepAliveInterval = TimeSpan.FromSeconds(0.25)
                }
                });
 
                client = new WebsocketClient(url, factory);
                client.DisconnectionHappened.Subscribe(info =>
                {
                    if (SocketCacheData.ContainsKey(url.ToString())) {
 
                        ConcurrentQueue<string> queue = null;
                        SocketCacheData.TryRemove(url.ToString(), out queue);
                    }// Console.WriteLine($"Disconnected: {info.Type}");
                });
 
                client.MessageReceived.Subscribe(msg =>
                {
                    if (msg == null)
                    {
                        // // Console.WriteLine("received: NULL");
                        return;
                    }
 
                    ConcurrentQueue<string> queue = null;
 
                    if (SocketCacheData.ContainsKey(url.ToString()))
                    {
                        queue = SocketCacheData[url.ToString()];
                    }
                    else
                    {
                        queue = new ConcurrentQueue<string>();
                        SocketCacheData[url.ToString()] = queue;
                    }
 
                    queue.Enqueue(msg.Text);
 
                });
 
                client.Start();
                SocketCache[url.ToString()] = client;
            }
 
            // Console.WriteLine("sending: " + cmd);
            Task.Run(() => client.Send(cmd));
            bool hadData = false;
            while (!Console.KeyAvailable)
            {
                ConcurrentQueue<string> queue = null;
                if (SocketCacheData.ContainsKey(url.ToString()))
                {
                    queue = SocketCacheData[url.ToString()];
                    hadData = true;
                }
                else
                {
                    if (hadData)
                    {
                        break;
                    }
                    queue = new ConcurrentQueue<string>();
                    SocketCacheData[url.ToString()] = queue;
                }
 
                string msg = null;
                if (queue.TryDequeue(out msg))
                {
                    if (msg.StartsWith("{\"id\":" + id.ToString()))
                    {
                        // Console.WriteLine("returned: " + msg);
                        return msg;
                    }
 
                    // Console.WriteLine("received: " + msg);
                    queue.Enqueue(msg);
                }
 
                if ((System.DateTime.UtcNow - lastRx).TotalSeconds > timeoutseconds)
                {
                    return null;
                }
 
                Thread.Sleep(10);
            }
 
            return null;
        }
 
        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)
        {
            lock (padLock)
                // Sometimes binding to localhost might resolve wrong AddressFamily, force IPv4
                this.sessionWSEndpoint = sessionWSEndpoint.Replace("ws://localhost", "ws://127.0.0.1").Replace("wss://localhost", "wss://127.0.0.1");
        }
    }
}