diff --git a/Runtime/ChromeDevtools/BrowserTab.cs b/Runtime/ChromeDevtools/BrowserTab.cs index d0c439b5f93ae4e849c3e87ee2104e0a9ffee822..4f0bbe53ecbdafd81372449c6966619e8d160dae 100644 --- a/Runtime/ChromeDevtools/BrowserTab.cs +++ b/Runtime/ChromeDevtools/BrowserTab.cs @@ -254,10 +254,8 @@ namespace bessw.Unity.WebView.ChromeDevTools public void Close() { Debug.Log($"BrowserTab close called for: '{pageTarget.Url}'"); - _ = devtools.SendCommandAsync(new closeTarget(pageTarget.Id), delegate - { - devtools.Dispose(); - }); + _ = devtools.SendCommandAsync(new closeTarget(pageTarget.Id)) + .ContinueWith(_ => devtools.Dispose()); } } } diff --git a/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs b/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs index 75bf0814df5b32860f1cffedec5c271414edc860..2fa4159e4d15be6032bd9e591a1b8ef66e9f76f2 100644 --- a/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs +++ b/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs @@ -7,7 +7,9 @@ using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading.Tasks; +using UnityEditor.PackageManager; using UnityEngine; namespace bessw.Unity.WebView.ChromeDevTools @@ -21,7 +23,7 @@ namespace bessw.Unity.WebView.ChromeDevTools private IDevtoolsConnection devtools; - private ConcurrentDictionary<long, ResponseTypeAndCallback> commandResponseDict = new ConcurrentDictionary<long, ResponseTypeAndCallback>(); + private ConcurrentDictionary<long, ResponseTypeAndCallback<IDevtoolsResponse>> commandResponseDict = new(); // devtools events public event Action<screencastFrameEvent> onScreencastFrame; @@ -102,18 +104,23 @@ namespace bessw.Unity.WebView.ChromeDevTools { // get the callback and the type for this response - ResponseTypeAndCallback responseTypeAndCallback; - if (!commandResponseDict.TryRemove(response.Id, out responseTypeAndCallback)) + if (!commandResponseDict.TryRemove(response.Id, out ResponseTypeAndCallback<IDevtoolsResponse> responseTypeAndCallback)) { Debug.Log($"There is no command waiting for the response '{response.Id}'"); return; } - // deserialize the result - IDevtoolsResponse commandResponse = (IDevtoolsResponse) response.Result.ToObject(responseTypeAndCallback.responseType, Browser.devtoolsSerializer); + if (response.IsError) + { + responseTypeAndCallback.Task.SetException(new DevtoolsCommandException(response.Error)); + } else + { + // deserialize the result + IDevtoolsResponse commandResponse = (IDevtoolsResponse)response.Result.ToObject(responseTypeAndCallback.ResponseType, Browser.devtoolsSerializer); - // pass the response to the callback - responseTypeAndCallback.callback(commandResponse); + // pass the response to the callback + responseTypeAndCallback.Task.SetResult(commandResponse); + } } /// <summary> @@ -192,12 +199,13 @@ namespace bessw.Unity.WebView.ChromeDevTools /// Coroutine wrapper for SendCommandAsync /// json serializes and sends a command over the devtools websocket /// </summary> - /// <typeparam name="T">IDevtoolsCommand</typeparam> + /// <typeparam name="C">IDevtoolsCommand</typeparam> /// <param name="command"></param> /// <returns></returns> - public IEnumerator SendCommand<T>(T command, Action<IDevtoolsResponse> callback) where T : IDevtoolsCommandWithResponse + public IEnumerator SendCommand<C>(C command, Action<IDevtoolsResponse> callback) where C : IDevtoolsCommandWithResponse { - var sendTask = SendCommandAsync(command, callback); + var sendTask = SendCommandAsync(command) + .ContinueWith(_ => callback); // wait until the command has been send yield return new WaitUntil(() => sendTask.IsCompleted); } @@ -205,61 +213,29 @@ namespace bessw.Unity.WebView.ChromeDevTools /// <summary> /// json serializes and sends a command over the devtools websocket /// </summary> - /// <typeparam name="C">IDevtoolsCommandWithResponse</typeparam> - /// <typeparam name="R">IDevtoolsResponse</typeparam> - /// <param name="command"></param> - /// <returns>Task that resoves with the response</returns> - public Task<R> SendCommandAsync<C, R>(C command) where C : IDevtoolsCommandWithResponse where R : class, IDevtoolsResponse - { - // apply the message wrapper - var wrappedCommand = new DevtoolsCommandWrapper<C>(command) - { - Id = ++LAST_COMMAND_ID - }; - - TaskCompletionSource<R> tcs = new TaskCompletionSource<R>(); - - // register the response callback for this command id - if (!commandResponseDict.TryAdd(wrappedCommand.Id, new ResponseTypeAndCallback(typeof(R), (res) => tcs.SetResult(res as R)))) - { - throw new InvalidOperationException($"could not add response callback for command '{wrappedCommand.Id}' to commandResponseDict"); - } - - // json serialize the command and send it - var json = JsonConvert.SerializeObject(wrappedCommand, Browser.devtoolsSerializerSettings); - Debug.Log($"ws send: '{json}'"); - devtools.SendCommandAsync(json); - return tcs.Task; - } + /// <typeparam name="C">IDevtoolsCommand</typeparam> + /// <returns>Task that resoves then the command is send</returns> + public Task<DevtoolsResponse> SendCommandAsync<C>(C command) where C : IDevtoolsCommand => SendCommandAsync<C, DevtoolsResponse>(command); /// <summary> /// json serializes and sends a command over the devtools websocket /// </summary> - /// <typeparam name="T">IDevtoolsCommand</typeparam> + /// <typeparam name="C">IDevtoolsCommandWithResponse</typeparam> + /// <typeparam name="R">IDevtoolsResponse</typeparam> /// <param name="command"></param> - /// <returns>Task that resoves then the command is send</returns> - public async Task SendCommandAsync<T>(T command, Action<IDevtoolsResponse> callback) where T : IDevtoolsCommandWithResponse + /// <returns>Task that resoves with the response</returns> + public Task<R> SendCommandAsync<C, R>(C command) where C : IDevtoolsCommand where R : class, IDevtoolsResponse { // apply the message wrapper - var wrappedCommand = new DevtoolsCommandWrapper<T>(command) + var wrappedCommand = new DevtoolsCommandWrapper<C>(command) { Id = ++LAST_COMMAND_ID }; - // get the response type from the commands attribute - CommandResponseAttribute cra = (CommandResponseAttribute)Attribute.GetCustomAttribute( - command.GetType(), - typeof(CommandResponseAttribute) - ); - if( cra == null ) - { - throw new NotImplementedException("command has no respnse attribute"); - } - // TODO: handle command without response - Type responseType = cra.responseType; + TaskCompletionSource<IDevtoolsResponse> response_tcs = new(); // register the response callback for this command id - if (!commandResponseDict.TryAdd(wrappedCommand.Id, new ResponseTypeAndCallback(responseType, callback))) + if (!commandResponseDict.TryAdd(wrappedCommand.Id, new ResponseTypeAndCallback<IDevtoolsResponse>(typeof(R), response_tcs))) { throw new InvalidOperationException($"could not add response callback for command '{wrappedCommand.Id}' to commandResponseDict"); } @@ -267,26 +243,9 @@ namespace bessw.Unity.WebView.ChromeDevTools // json serialize the command and send it var json = JsonConvert.SerializeObject(wrappedCommand, Browser.devtoolsSerializerSettings); Debug.Log($"ws send: '{json}'"); - await devtools.SendCommandAsync(json); - } - - /// <summary> - /// json serializes and sends a command over the devtools websocket - /// </summary> - /// <typeparam name="T">IDevtoolsCommand</typeparam> - /// <returns>Task that resoves then the command is send</returns> - public async Task SendCommandAsync<T>(T command) where T : IDevtoolsCommand - { - // apply the message wrapper - var wrappedCommand = new DevtoolsCommandWrapper<T>(command) - { - Id = ++LAST_COMMAND_ID - }; + devtools.SendCommandAsync(json); - // json serialize the command and send it - var json = JsonConvert.SerializeObject(wrappedCommand, Browser.devtoolsSerializerSettings); - Debug.Log($"ws send: '{json}'"); - await devtools.SendCommandAsync(json); + return response_tcs.Task.ContinueWith(task => (R) task.Result); } public void Dispose() @@ -298,25 +257,34 @@ namespace bessw.Unity.WebView.ChromeDevTools ///////////////////////////// // Utility classes: - /// <summary> /// Container type to store a response type together with its callback. /// This is used in DevtoolsProtocolHandler to parse the response to the correct C# data type and call the callback with it as parameter. /// </summary> - record ResponseTypeAndCallback + record ResponseTypeAndCallback<RESPONSE_TYPE> where RESPONSE_TYPE : class, IDevtoolsResponse { - public ResponseTypeAndCallback(Type responseType, Action<IDevtoolsResponse> callback) + public ResponseTypeAndCallback(Type responseType, TaskCompletionSource<RESPONSE_TYPE> task) { - this.responseType = responseType; - this.callback = callback; + this.ResponseType = responseType; + this.Task = task; } - public Type responseType { get; } - public Action<IDevtoolsResponse> callback { get; } + public Type ResponseType { get; } + public TaskCompletionSource<RESPONSE_TYPE> Task { get; } } + public class UnexpectedMessageException : DevtoolsConnectionException { public UnexpectedMessageException(string message) : base($"Unexpected message {message}") { } } + public class DevtoolsCommandException : DevtoolsConnectionException + { + public readonly long Code; + public DevtoolsCommandException(DevtoolsErrorResponse error) : base(error.Message) + { + Code = error.Code; + } + } + } \ No newline at end of file diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs b/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs index 5c254125b7855e978862f296ddb21b9667c7a529..b53fdc206b1ce4a301a25eb9c0ccbd6e903a83f6 100644 --- a/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs +++ b/Runtime/ChromeDevtools/Protocol/DevtoolsEvent.cs @@ -7,13 +7,6 @@ namespace bessw.Unity.WebView.ChromeDevTools.Protocol public string Method { get; set; } public JObject Params { get; set; } } - /// - /// Every devtools command response has the same id and a method as the corresponding command - /// - public class DevtoolsEventWrapper<T> : DevtoolsEventWrapper where T : IDevtoolsEvent - { - public new T Params { get; set; } - } public interface IDevtoolsEvent { } } diff --git a/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs b/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs index def8bdbba961ec86d3e70b2c05e31347f4755622..767e01d889d4c659f9a69a72b8eb4a4dcda60ce4 100644 --- a/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs +++ b/Runtime/ChromeDevtools/Protocol/DevtoolsResponse.cs @@ -1,19 +1,25 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +#nullable enable annotations namespace bessw.Unity.WebView.ChromeDevTools.Protocol { public class DevtoolsResponseWrapper { public long Id { get; set; } - public JObject Result { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public JObject? Result { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public DevtoolsErrorResponse? Error { get; set; } + [JsonIgnore] + public bool IsError => Error is not null; } - /// - /// Every devtools command response has the same id and a method as the corresponding command - /// - public class DevtoolsResponseWrapper<T> : DevtoolsResponseWrapper where T : IDevtoolsResponse + public class DevtoolsErrorResponse { - public new T Result { get; set; } + public long Code { get; set; } + public string Message { get; set; } } public interface IDevtoolsResponse { } + public class DevtoolsResponse : IDevtoolsResponse { } }