From 1f7f16f614025e0ceb8b2c0da9871fabd25ae5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Juri=C4=87?= Date: Fri, 10 Nov 2017 00:50:41 +0100 Subject: [PATCH] Enhancing robustness. Enhancing error handling. Adding RPCJs settings. --- Samples/ClientJs/Program.cs | 3 +- Samples/ClientJs/ServerClientJs.csproj | 2 + Samples/MultiService/MultiService.csproj | 2 + Samples/Serialization/Serialization.csproj | 2 + .../WebSocketRPC.AspCore.csproj | 3 + .../ClientServer/Connection.cs | 44 ++++++--- .../ConnectionBinders/Base/Binder.cs | 8 +- Source/WebSocketRPC.Base/Invokers/Messages.cs | 10 +- Source/WebSocketRPC.Base/Utils/TaskQueue.cs | 36 +++++++ .../WebSocketRPC.Base.projitems | 1 + .../Components/JsCallerGenerator.cs | 16 +++- .../Components/JsDocGenerator.cs | 2 +- Source/WebSocketRPC.JS/RPCJs.cs | 29 +++--- Source/WebSocketRPC.JS/RPCJsSettings.cs | 79 +++++++++++++++ Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj | 5 +- .../ClientServer/Server.cs | 96 ++++++++++++++----- .../WebsocketRPC.Standalone.csproj | 3 + 17 files changed, 279 insertions(+), 62 deletions(-) create mode 100644 Source/WebSocketRPC.Base/Utils/TaskQueue.cs create mode 100644 Source/WebSocketRPC.JS/RPCJsSettings.cs diff --git a/Samples/ClientJs/Program.cs b/Samples/ClientJs/Program.cs index 85a2b74..549b7ae 100644 --- a/Samples/ClientJs/Program.cs +++ b/Samples/ClientJs/Program.cs @@ -52,11 +52,12 @@ static void Main(string[] args) //start server and bind its local and remote API var cts = new CancellationTokenSource(); - Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => c.Bind(new LocalAPI())).Wait(0); + var s = Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => c.Bind(new LocalAPI())); Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(TestClientJs)); Console.ReadLine(); cts.Cancel(); + s.Wait(); } } } diff --git a/Samples/ClientJs/ServerClientJs.csproj b/Samples/ClientJs/ServerClientJs.csproj index f77e196..db4f0f9 100644 --- a/Samples/ClientJs/ServerClientJs.csproj +++ b/Samples/ClientJs/ServerClientJs.csproj @@ -8,6 +8,8 @@ Exe + + bin\$(TargetFramework)\ServerClientJs.xml diff --git a/Samples/MultiService/MultiService.csproj b/Samples/MultiService/MultiService.csproj index 739137d..e641832 100644 --- a/Samples/MultiService/MultiService.csproj +++ b/Samples/MultiService/MultiService.csproj @@ -8,6 +8,8 @@ Exe + + bin\$(TargetFramework)\MultiService.xml diff --git a/Samples/Serialization/Serialization.csproj b/Samples/Serialization/Serialization.csproj index 0d7a196..b00b4f7 100644 --- a/Samples/Serialization/Serialization.csproj +++ b/Samples/Serialization/Serialization.csproj @@ -9,6 +9,8 @@ Exe + + bin\$(TargetFramework)\Serialization.xml diff --git a/Source/WebSocketRPC.AspCore/WebSocketRPC.AspCore.csproj b/Source/WebSocketRPC.AspCore/WebSocketRPC.AspCore.csproj index 7b70f0b..158fd2c 100644 --- a/Source/WebSocketRPC.AspCore/WebSocketRPC.AspCore.csproj +++ b/Source/WebSocketRPC.AspCore/WebSocketRPC.AspCore.csproj @@ -35,6 +35,9 @@ 1.0.0 ../../Deploy/Nuget/bin/ WebSocketRPC + + True + True diff --git a/Source/WebSocketRPC.Base/ClientServer/Connection.cs b/Source/WebSocketRPC.Base/ClientServer/Connection.cs index f3cd3d3..fd25494 100644 --- a/Source/WebSocketRPC.Base/ClientServer/Connection.cs +++ b/Source/WebSocketRPC.Base/ClientServer/Connection.cs @@ -42,6 +42,7 @@ public class Connection static string messageToBig = "The message exceeds the maximum allowed message size: {0} bytes."; WebSocket socket; + TaskQueue sendTaskQueue; /// /// Creates new connection. @@ -51,6 +52,7 @@ public class Connection internal protected Connection(WebSocket socket, IReadOnlyDictionary cookies) { this.socket = socket; + this.sendTaskQueue = new TaskQueue(); this.Cookies = cookies; } @@ -92,7 +94,8 @@ public async Task SendAsync(ArraySegment data) return false; } - await socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); + Debug.WriteLine("Sending binary data."); + await sendTaskQueue.Enqueue(() => sendAsync(data, WebSocketMessageType.Binary)); return true; } @@ -116,10 +119,23 @@ public async Task SendAsync(string data, Encoding e) } Debug.WriteLine("Sending: " + data); - await socket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None); + await sendTaskQueue.Enqueue(() => sendAsync(segment, WebSocketMessageType.Text)); return true; } + async Task sendAsync(ArraySegment data, WebSocketMessageType msgType) + { + try + { + await socket.SendAsync(data, msgType, true, CancellationToken.None); + } + catch(Exception ex) + { + if (socket.State != WebSocketState.Open) + await CloseAsync(WebSocketCloseStatus.InternalServerError, ex.Message); + } + } + /// /// Closes the connection. /// @@ -128,12 +144,18 @@ public async Task SendAsync(string data, Encoding e) /// Task. public async Task CloseAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure, string statusDescription = "") { - if (socket.State != WebSocketState.Open) - return; - - await socket.CloseOutputAsync(closeStatus, statusDescription, CancellationToken.None); - OnClose?.Invoke(); - clearEvents(); + try + { + if (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseReceived) + await socket.CloseOutputAsync(closeStatus, statusDescription, CancellationToken.None); + } + catch + { } //do not propagate the exception + finally + { + OnClose?.Invoke(); + clearEvents(); + } } /// @@ -151,7 +173,7 @@ internal static async Task ListenReceiveAsync(Connection connection, Cancellatio { connection.OnOpen?.Invoke(); byte[] receiveBuffer = new byte[RPCSettings.MaxMessageSize]; - + while (webSocket.State == WebSocketState.Open) { WebSocketReceiveResult receiveResult = null; @@ -186,9 +208,8 @@ internal static async Task ListenReceiveAsync(Connection connection, Cancellatio } catch (Exception ex) { - while (ex.InnerException != null) ex = ex.InnerException; connection.OnError?.Invoke(ex); - connection.OnClose?.Invoke(); + await connection.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.Message); //socket will be aborted -> no need to close manually } } @@ -205,7 +226,6 @@ internal void InvokeErrorAsync(Exception ex) private void clearEvents() { - OnOpen = null; OnClose = null; OnError = null; OnReceive = null; diff --git a/Source/WebSocketRPC.Base/ConnectionBinders/Base/Binder.cs b/Source/WebSocketRPC.Base/ConnectionBinders/Base/Binder.cs index 6f64eca..b5cfaa9 100644 --- a/Source/WebSocketRPC.Base/ConnectionBinders/Base/Binder.cs +++ b/Source/WebSocketRPC.Base/ConnectionBinders/Base/Binder.cs @@ -36,13 +36,7 @@ abstract class Binder : IBinder protected Binder(Connection connection) { Connection = connection; - - Connection.OnOpen += () => - { - Debug.WriteLine("Open"); - - RPC.AllBinders.Add(this); - }; + RPC.AllBinders.Add(this); Connection.OnClose += () => { diff --git a/Source/WebSocketRPC.Base/Invokers/Messages.cs b/Source/WebSocketRPC.Base/Invokers/Messages.cs index 6e28a5b..cdd365d 100644 --- a/Source/WebSocketRPC.Base/Invokers/Messages.cs +++ b/Source/WebSocketRPC.Base/Invokers/Messages.cs @@ -36,7 +36,10 @@ struct Request public static Request FromJson(string json) { - var root = JObject.Parse(json); + JObject root = null; + try { root = JObject.Parse(json); } + catch { return default(Request); } + var r = new Request { FunctionName = root[nameof(FunctionName)]?.Value(), @@ -65,7 +68,10 @@ struct Response public static Response FromJson(string json) { - var root = JObject.Parse(json); + JObject root = null; + try { root = JObject.Parse(json); } + catch { return default(Response); } + var r = new Response { FunctionName = root[nameof(FunctionName)]?.Value(), diff --git a/Source/WebSocketRPC.Base/Utils/TaskQueue.cs b/Source/WebSocketRPC.Base/Utils/TaskQueue.cs new file mode 100644 index 0000000..1d96f45 --- /dev/null +++ b/Source/WebSocketRPC.Base/Utils/TaskQueue.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace WebSocketRPC +{ + //taken from: https://stackoverflow.com/questions/25691679/best-way-in-net-to-manage-queue-of-tasks-on-a-separate-single-thread and modified + class TaskQueue + { + private SemaphoreSlim semaphore; + public TaskQueue() + { + semaphore = new SemaphoreSlim(1); + } + + public async Task Enqueue(Func func) + { + await semaphore.WaitAsync(); + try + { + await func(); + } + finally + { + semaphore.Release(); + } + } + + ~TaskQueue() + { + //dispose pattern is not needed because' AvailableWaitHandle' is not used + //ref: https://stackoverflow.com/questions/32033416/do-i-need-to-dispose-a-semaphoreslim + semaphore.Dispose(); + } + } +} diff --git a/Source/WebSocketRPC.Base/WebSocketRPC.Base.projitems b/Source/WebSocketRPC.Base/WebSocketRPC.Base.projitems index 8ff1a33..6b3995a 100644 --- a/Source/WebSocketRPC.Base/WebSocketRPC.Base.projitems +++ b/Source/WebSocketRPC.Base/WebSocketRPC.Base.projitems @@ -23,5 +23,6 @@ + \ No newline at end of file diff --git a/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs b/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs index 6279221..aca460c 100644 --- a/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs +++ b/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs @@ -67,6 +67,20 @@ public static (string, MethodInfo[]) GetMethods(params Expression>[ return (objType.Name, methodList); } + public static string GenerateRequireJsHeader(string className) + { + var t = new string[] { + $"define(() => {className}); //'require.js' support" + }; + + var sb = new StringBuilder(); + sb.Append(String.Join(Environment.NewLine, t)); + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + + return sb.ToString(); + } + public static string GenerateHeader(string className) { var t = new string[] { @@ -107,7 +121,7 @@ public static string GenerateMethod(string methodName, string[] argNames) $"\t this.{jsMName} = function({argList}) {{", $"\t return callRPC(\"{methodName}\", Object.values(this.{jsMName}.arguments));", $"\t }};", - $"" + $"{Environment.NewLine}" }; var sb = new StringBuilder(); diff --git a/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs b/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs index 8f4ddc1..5de4ae1 100644 --- a/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs +++ b/Source/WebSocketRPC.JS/Components/JsDocGenerator.cs @@ -77,13 +77,13 @@ public static string GetMethodDoc(XmlNodeList mmebers, string methodName, IList pNames, IList pTypes, Type returnType, string linePrefix = "\t") { var mElem = getMethod(mmebers, methodName); + if (mElem == null) return String.Empty; var s = getSummary(mElem); var p = getParams(mElem); var r = getReturn(mElem); var jsDoc = new StringBuilder(); - jsDoc.AppendLine(); jsDoc.AppendLine(String.Format("{0}/**", linePrefix)); { jsDoc.AppendLine(String.Format("{0} * @description - {1}", linePrefix, s)); diff --git a/Source/WebSocketRPC.JS/RPCJs.cs b/Source/WebSocketRPC.JS/RPCJs.cs index e85bb56..19ef08e 100644 --- a/Source/WebSocketRPC.JS/RPCJs.cs +++ b/Source/WebSocketRPC.JS/RPCJs.cs @@ -26,7 +26,6 @@ using System; using System.IO; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -41,13 +40,16 @@ public static class RPCJs /// Generates Javascript code from the provided class or interface type. /// /// Class or interface type. - /// The methods of the class / interface that should be omitted when creating the Javascript code. + /// RPC-Js settings used for Javascript code generation. /// Javascript API. - public static string GenerateCaller(params Expression>[] omittedMethods) + public static string GenerateCaller(RPCJsSettings settings = null) { - var (tName, mInfos) = JsCallerGenerator.GetMethods(omittedMethods); + settings = settings ?? new RPCJsSettings(); + var (tName, mInfos) = JsCallerGenerator.GetMethods(settings.OmittedMethods); + tName = settings.NameOverwrite ?? tName; var sb = new StringBuilder(); + if(settings.WithRequireSupport) sb.Append(JsCallerGenerator.GenerateRequireJsHeader(tName)); sb.Append(JsCallerGenerator.GenerateHeader(tName)); foreach (var m in mInfos) @@ -67,15 +69,18 @@ public static string GenerateCaller(params Expression>[] omittedMet /// /// Class or interface type. /// Xml assembly definition file. - /// The methods of the class / interface that should be omitted when creating the Javascript code. + /// RPC-Js settings used for Javascript code generation. /// Javascript API. - public static string GenerateCallerWithDoc(string xmlDocPath, params Expression>[] omittedMethods) + public static string GenerateCallerWithDoc(string xmlDocPath, RPCJsSettings settings = null) { - var (tName, mInfos) = JsCallerGenerator.GetMethods(omittedMethods); + settings = settings ?? new RPCJsSettings(); + var (tName, mInfos) = JsCallerGenerator.GetMethods(settings.OmittedMethods); + tName = settings.NameOverwrite ?? tName; var xmlMemberNodes = JsDocGenerator.GetMemberNodes(xmlDocPath); - var sb = new StringBuilder(); + var sb = new StringBuilder(); + if (settings.WithRequireSupport) sb.Append(JsCallerGenerator.GenerateRequireJsHeader(tName)); sb.Append(JsDocGenerator.GetClassDoc(xmlMemberNodes, tName)); sb.Append(JsCallerGenerator.GenerateHeader(tName)); @@ -98,9 +103,9 @@ public static string GenerateCallerWithDoc(string xmlDocPath, params Expressi /// The xml assembly definition is taken form the executing assembly if available. /// /// Class or interface type. - /// The methods of the class / interface that should be omitted when creating the Javascript code. + /// RPC-Js settings used for Javascript code generation. /// Javascript API. - public static string GenerateCallerWithDoc(params Expression>[] omittedMethods) + public static string GenerateCallerWithDoc(RPCJsSettings settings = null) { var assembly = Assembly.GetEntryAssembly(); var fInfo = new FileInfo(assembly.Location); @@ -108,9 +113,9 @@ public static string GenerateCallerWithDoc(params Expression>[] omi var xmlDocPath = Path.ChangeExtension(assembly.Location, ".xml"); if (!File.Exists(xmlDocPath)) - return GenerateCaller(omittedMethods); + return GenerateCaller(settings); else - return GenerateCallerWithDoc(xmlDocPath, omittedMethods); + return GenerateCallerWithDoc(xmlDocPath, settings); } } } diff --git a/Source/WebSocketRPC.JS/RPCJsSettings.cs b/Source/WebSocketRPC.JS/RPCJsSettings.cs new file mode 100644 index 0000000..f68424d --- /dev/null +++ b/Source/WebSocketRPC.JS/RPCJsSettings.cs @@ -0,0 +1,79 @@ +#region License +// Copyright © 2017 Darko Jurić +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Linq.Expressions; +using System.Text.RegularExpressions; + +namespace WebSocketRPC +{ + /// + /// RPC-Js settings used for Javascript code generation. + /// + /// Class or interface type. + public class RPCJsSettings + { + Expression>[] omittedMethods = new Expression>[0]; + /// + /// Gets or sets the methods of the class / interface that should be omitted when creating the Javascript code. + /// + public Expression>[] OmittedMethods + { + get { return omittedMethods; } + set + { + if (omittedMethods == null) + throw new ArgumentException("The value must be not null."); + + omittedMethods = value; + } + } + + /// + /// if true the 'require.js' header will be present in the generated files. + /// + public bool WithRequireSupport { get; set; } + + string nameOverwrite = null; + /// + /// Javascript API name. If null, the name is the same as the corresponding .NET type name. + /// + public string NameOverwrite + { + get { return nameOverwrite; } + set + { + if (value != null) + { + var isValid = Regex.Match(value, "^[a-zA-Z_][a-zA-Z0-9_]*$").Success; + if (!isValid) + throw new ArgumentException("The name-overwrite must be valid Javascript name or null (if autogenerated)."); + } + + nameOverwrite = value; + } + } + } +} diff --git a/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj b/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj index 954dc88..e665441 100644 --- a/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj +++ b/Source/WebSocketRPC.JS/WebSocketRPC.JS.csproj @@ -30,7 +30,10 @@ 1.0.0 ../../Deploy/Nuget/bin/ - WebSocketRPC + WebSocketRPC + + True + True \ No newline at end of file diff --git a/Source/WebsocketRPC.Standalone/ClientServer/Server.cs b/Source/WebsocketRPC.Standalone/ClientServer/Server.cs index f8d4f80..e807610 100644 --- a/Source/WebsocketRPC.Standalone/ClientServer/Server.cs +++ b/Source/WebsocketRPC.Standalone/ClientServer/Server.cs @@ -36,6 +36,34 @@ namespace WebSocketRPC /// public static class Server { + /// + /// Creates and starts a new instance of the http / websocket server. + /// + /// The http/https URI listening port. + /// Cancellation token. + /// Action executed when connection is created. + /// Server task. + public static async Task ListenAsync(int port, CancellationToken token, Action onConnect) + { + await ListenAsync($"http://+:{port}/", token, onConnect); + } + + /// + /// Creates and starts a new instance of the http / websocket server. + /// All HTTP requests will have the 'BadRequest' response. + /// + /// The http/https URI listening port. + /// Cancellation token. + /// Action executed when connection is created. + /// Action executed on HTTP request. + /// Server task. + public static async Task ListenAsync(int port, CancellationToken token, Action onConnect, Action onHttpRequest) + { + await ListenAsync($"http://+:{port}/", token, onConnect, onHttpRequest); + } + + + /// /// Creates and starts a new instance of the http / websocket server. /// @@ -64,52 +92,70 @@ public static async Task ListenAsync(string httpListenerPrefix, CancellationToke { var listener = new HttpListener(); listener.Prefixes.Add(httpListenerPrefix); - listener.Start(); - - while (true) + + try { listener.Start(); } + catch (Exception ex) when ((ex as HttpListenerException)?.ErrorCode == 5) { - HttpListenerContext listenerContext = await listener.GetContextAsync(); + throw new UnauthorizedAccessException($"The HTTP server can not be started, as the namespace reservation does not exist.\n" + + $"Please run (elevated): 'netsh add urlacl url={httpListenerPrefix} user=\"Everyone\"'."); + } - if (listenerContext.Request.IsWebSocketRequest) - { - listenAsync(listenerContext, token, onConnect).Wait(0); - } - else + ///using (var r = token.Register(() => listener.Stop())) + { + bool shouldStop = false; + while (!shouldStop) { - onHttpRequest(listenerContext.Request, listenerContext.Response); - listenerContext.Response.Close(); - } + try + { + HttpListenerContext ctx = await listener.GetContextAsync(); - if (token.IsCancellationRequested) - break; + if (ctx.Request.IsWebSocketRequest) + { + listenAsync(ctx, token, onConnect).Wait(0); + } + else + { + onHttpRequest(ctx.Request, ctx.Response); + ctx.Response.Close(); + } + } + catch (Exception) + { + if (!token.IsCancellationRequested) + throw; + } + finally + { + if (token.IsCancellationRequested) + shouldStop = true; + } + } } - - listener.Stop(); } - static async Task listenAsync(HttpListenerContext listenerContext, CancellationToken token, Action onConnect) + static async Task listenAsync(HttpListenerContext ctx, CancellationToken token, Action onConnect) { - if (!listenerContext.Request.IsWebSocketRequest) + if (!ctx.Request.IsWebSocketRequest) return; - WebSocketContext ctx = null; + WebSocketContext wsCtx = null; WebSocket webSocket = null; try { - ctx = await listenerContext.AcceptWebSocketAsync(subProtocol: null); - webSocket = ctx.WebSocket; + wsCtx = await ctx.AcceptWebSocketAsync(subProtocol: null); + webSocket = wsCtx.WebSocket; } catch (Exception) { - listenerContext.Response.StatusCode = 500; - listenerContext.Response.Close(); + ctx.Response.StatusCode = 500; + ctx.Response.Close(); return; } - var connection = new Connection(webSocket, CookieUtils.GetCookies(ctx.CookieCollection)); + var connection = new Connection(webSocket, CookieUtils.GetCookies(wsCtx.CookieCollection)); try { - onConnect(connection, ctx); + onConnect(connection, wsCtx); await Connection.ListenReceiveAsync(connection, token); } finally diff --git a/Source/WebsocketRPC.Standalone/WebsocketRPC.Standalone.csproj b/Source/WebsocketRPC.Standalone/WebsocketRPC.Standalone.csproj index bf31d54..451c808 100644 --- a/Source/WebsocketRPC.Standalone/WebsocketRPC.Standalone.csproj +++ b/Source/WebsocketRPC.Standalone/WebsocketRPC.Standalone.csproj @@ -32,6 +32,9 @@ 1.0.0 ../../Deploy/Nuget/bin/ + + True + True