├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── WebSocketManager.sln ├── samples ├── .DS_Store ├── ChatApplication │ ├── ChatApplication.csproj │ ├── ChatHandler.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── wwwroot │ │ ├── WebSocketManager.js │ │ └── client.html ├── EchoConsoleClient │ ├── EchoConsoleClient.csproj │ └── Program.cs ├── MvcSample │ ├── Controllers │ │ └── MessagesController.cs │ ├── MessageHandlers │ │ └── NotificationsMessageHandler.cs │ ├── MvcSample.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── wwwroot │ │ └── index.html ├── WebSocketManagerSamples.sln └── WebTerm │ ├── ConsoleAppManager.cs │ ├── NOTICE │ ├── Program.cs │ ├── Startup.cs │ ├── WebTermHandler.cs │ └── wwwroot │ ├── FileBrowser.css │ ├── Index.html │ ├── jquery.console.js │ ├── jquery │ ├── .bower.json │ ├── LICENSE.txt │ └── dist │ │ └── jquery.js │ └── webterm.js ├── src ├── WebSocketManager.Client.TS │ ├── WebSocketManager.Client.TS.njsproj │ ├── dist │ │ └── WebSocketManager.js │ ├── package.json │ ├── src │ │ ├── Connection.js │ │ ├── Connection.ts │ │ ├── InvocationDescriptor.js │ │ ├── InvocationDescriptor.ts │ │ ├── Message.js │ │ └── Message.ts │ ├── tsconfig.json │ ├── tslint.json │ └── webpack.config.js ├── WebSocketManager.Client │ ├── Connection.cs │ └── WebSocketManager.Client.csproj ├── WebSocketManager.Common │ ├── Json │ │ ├── JsonBinderWithoutAssembly.cs │ │ └── PrimitiveJsonConverter.cs │ ├── Networking │ │ ├── InvocationDescriptor.cs │ │ ├── InvocationResult.cs │ │ ├── Message.cs │ │ └── RemoteException.cs │ ├── Strategies │ │ ├── ControllerMethodInvocationStrategy.cs │ │ ├── DecoratedControllerMethodInvocationStrategy.cs │ │ ├── MethodInvocationStrategy.cs │ │ └── StringMethodInvocationStrategy.cs │ └── WebSocketManager.Common.csproj └── WebSocketManager │ ├── WebSocketConnectionManager.cs │ ├── WebSocketHandler.cs │ ├── WebSocketManager.csproj │ ├── WebSocketManagerExtensions.cs │ └── WebSocketManagerMiddleware.cs └── test └── WebSocketManager.Tests ├── Helpers └── FakeSocket.cs ├── WebSocketConnectionManagerTests.cs └── WebSocketManager.Tests.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | .vscode/ 255 | 256 | src/WebSocketManager.Client.TS/app/build/ 257 | 258 | *.map 259 | *.min.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: trusty 4 | mono: none 5 | dotnet: 2.0.0 6 | 7 | 8 | addons: 9 | apt: 10 | sources: 11 | - sourceline: 'deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet-release/ trusty main' 12 | key_url: 'https://apt-mo.trafficmanager.net/keys/microsoft.asc' 13 | packages: 14 | - dotnet-sdk-2.0.0 15 | 16 | before_script: 17 | - dotnet restore 18 | - dotnet restore samples/WebSocketManagerSamples.sln 19 | 20 | script: 21 | - dotnet build 22 | - dotnet build samples/WebSocketManagerSamples.sln 23 | - dotnet test test/WebSocketManager.Tests/WebSocketManager.Tests.csproj 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Radu Matei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # websocket-manager 2 | 3 | Travis: [![Build Status](https://travis-ci.org/radu-matei/websocket-manager.svg?branch=master)](https://travis-ci.org/radu-matei/websocket-manager) 4 | 5 | NuGet: [![NuGet](https://img.shields.io/nuget/v/WebSocketManager.svg)](https://www.nuget.org/packages/WebSocketManager) 6 | 7 | Simple middlware for real-time .NET Core 8 | ---------------------------------------- 9 | 10 | This is an Asp .Net Core middleware that provides real-time functionality to .NET Core applications. 11 | 12 | To the core, it is a WebSocket middleware for Asp .Net Core with TypeScript / JavaScript client and .Net Core client that supports the client and the server invoking each others' methods. 13 | 14 | Why wouldn't I use SignalR for this? 15 | ------------------------------------ 16 | 17 | First of all, SignalR for Asp .Net Core is still in its very incipient stages. A preview is expected mid-2017, while a release near the end of 2017, so most probably it will be available for Asp .Net Core 2.0. 18 | 19 | > The preview and release information were taken from [this talk by Damian Edwards and David Fowler, the guys in charge of Asp .Net Core](https://vimeo.com/204078084). 20 | 21 | What is this library's connection to SignalR? 22 | ---------------------------------------------- 23 | 24 | This library **is not an official release by Microsoft** and in any way related to the original SignalR project, other by a lot of concepts inspired from it. 25 | 26 | Because the release of SignalR for Asp .Net Core was delayed for so long, I decided to write a very basic, stripped down (compared to the original SignalR) that only supports WebSockets (is based on `Microsoft.AspNetCore.WebSockets`) with a TypeScript client. 27 | 28 | A lot of features, both on the server side and the client side were written looking at SignalR (both old and new) code, so if you wrote SignalR in the past, the approach is very similar. 29 | 30 | Get Started with **websocket-manager** 31 | -------------------------------------- 32 | 33 | While it is still in development, you can see some examples of usage in the [`samples`](/samples) folder. 34 | 35 | Contribute to **websocket-manager** 36 | ----------------------------------- 37 | 38 | Contributions in any form are welcome! Submit issues with bugs and recommendations! 39 | **Pull Requests** are highly appreciated! -------------------------------------------------------------------------------- /WebSocketManager.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.9 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketManager.Common", "src\WebSocketManager.Common\WebSocketManager.Common.csproj", "{E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketManager", "src\WebSocketManager\WebSocketManager.csproj", "{633F738C-659D-40A5-B46E-3C9B28F279A6}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketManager.Client", "src\WebSocketManager.Client\WebSocketManager.Client.csproj", "{2D78B356-07F6-421A-9717-B7CD63897263}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketManager.Tests", "test\WebSocketManager.Tests\WebSocketManager.Tests.csproj", "{055CA15E-2D9C-4305-AC65-A58CC89F3072}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Debug|x64.ActiveCfg = Debug|Any CPU 27 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Debug|x64.Build.0 = Debug|Any CPU 28 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Debug|x86.ActiveCfg = Debug|Any CPU 29 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Debug|x86.Build.0 = Debug|Any CPU 30 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Release|x64.ActiveCfg = Release|Any CPU 33 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Release|x64.Build.0 = Release|Any CPU 34 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Release|x86.ActiveCfg = Release|Any CPU 35 | {E02806BF-EA9F-4A4C-86ED-68D68BA49ABF}.Release|x86.Build.0 = Release|Any CPU 36 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Debug|x64.Build.0 = Debug|Any CPU 40 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Debug|x86.Build.0 = Debug|Any CPU 42 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Release|x64.ActiveCfg = Release|Any CPU 45 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Release|x64.Build.0 = Release|Any CPU 46 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Release|x86.ActiveCfg = Release|Any CPU 47 | {633F738C-659D-40A5-B46E-3C9B28F279A6}.Release|x86.Build.0 = Release|Any CPU 48 | {2D78B356-07F6-421A-9717-B7CD63897263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {2D78B356-07F6-421A-9717-B7CD63897263}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {2D78B356-07F6-421A-9717-B7CD63897263}.Debug|x64.ActiveCfg = Debug|Any CPU 51 | {2D78B356-07F6-421A-9717-B7CD63897263}.Debug|x64.Build.0 = Debug|Any CPU 52 | {2D78B356-07F6-421A-9717-B7CD63897263}.Debug|x86.ActiveCfg = Debug|Any CPU 53 | {2D78B356-07F6-421A-9717-B7CD63897263}.Debug|x86.Build.0 = Debug|Any CPU 54 | {2D78B356-07F6-421A-9717-B7CD63897263}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {2D78B356-07F6-421A-9717-B7CD63897263}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {2D78B356-07F6-421A-9717-B7CD63897263}.Release|x64.ActiveCfg = Release|Any CPU 57 | {2D78B356-07F6-421A-9717-B7CD63897263}.Release|x64.Build.0 = Release|Any CPU 58 | {2D78B356-07F6-421A-9717-B7CD63897263}.Release|x86.ActiveCfg = Release|Any CPU 59 | {2D78B356-07F6-421A-9717-B7CD63897263}.Release|x86.Build.0 = Release|Any CPU 60 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Debug|x64.ActiveCfg = Debug|Any CPU 63 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Debug|x64.Build.0 = Debug|Any CPU 64 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Debug|x86.ActiveCfg = Debug|Any CPU 65 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Debug|x86.Build.0 = Debug|Any CPU 66 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Release|x64.ActiveCfg = Release|Any CPU 69 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Release|x64.Build.0 = Release|Any CPU 70 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Release|x86.ActiveCfg = Release|Any CPU 71 | {055CA15E-2D9C-4305-AC65-A58CC89F3072}.Release|x86.Build.0 = Release|Any CPU 72 | EndGlobalSection 73 | GlobalSection(SolutionProperties) = preSolution 74 | HideSolutionNode = FALSE 75 | EndGlobalSection 76 | GlobalSection(MonoDevelopProperties) = preSolution 77 | Policies = $0 78 | $0.StandardHeader = $1 79 | version = 0.2 80 | EndGlobalSection 81 | EndGlobal 82 | -------------------------------------------------------------------------------- /samples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radu-matei/websocket-manager/7c62da3643a562b4a441ce095f15a6557f76e9ee/samples/.DS_Store -------------------------------------------------------------------------------- /samples/ChatApplication/ChatApplication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | 0.2 6 | 7 | 8 | 9 | ..\..\..\websocke t-manager\samples\ChatApplication\bin\Debug\netcoreapp2.0 10 | 11 | 12 | ..\..\..\websocke t-manager\samples\ChatApplication\bin\Release\netcoreapp2.0 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/ChatApplication/ChatHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading.Tasks; 4 | using WebSocketManager; 5 | using WebSocketManager.Common; 6 | 7 | namespace ChatApplication 8 | { 9 | public class ChatHandler : WebSocketHandler 10 | { 11 | public ChatHandler(WebSocketConnectionManager webSocketConnectionManager) : base(webSocketConnectionManager, new ControllerMethodInvocationStrategy()) 12 | { 13 | ((ControllerMethodInvocationStrategy)MethodInvocationStrategy).Controller = this; 14 | } 15 | 16 | public override async Task OnConnected(WebSocket socket) 17 | { 18 | await base.OnConnected(socket); 19 | 20 | var socketId = WebSocketConnectionManager.GetId(socket); 21 | 22 | var message = new Message() 23 | { 24 | MessageType = MessageType.Text, 25 | Data = $"{socketId} is now connected" 26 | }; 27 | 28 | await SendMessageToAllAsync(message); 29 | } 30 | 31 | // this method can be called from a client, doesn't return anything. 32 | public async Task SendMessage(WebSocket socket, string message) 33 | { 34 | // chat command. 35 | if (message == "/math") 36 | { 37 | await AskClientToDoMath(socket); 38 | } 39 | else 40 | { 41 | await InvokeClientMethodToAllAsync("receiveMessage", WebSocketConnectionManager.GetId(socket), message); 42 | } 43 | } 44 | 45 | // this method can be called from a client, returns the integer result or throws an exception. 46 | public int DoMath(WebSocket socket, int a, int b) 47 | { 48 | if (a == 0 || b == 0) throw new Exception("That makes no sense."); 49 | return a + b; 50 | } 51 | 52 | // we ask a client to do some math for us then broadcast the results. 53 | private async Task AskClientToDoMath(WebSocket socket) 54 | { 55 | string id = WebSocketConnectionManager.GetId(socket); 56 | try 57 | { 58 | int result = await InvokeClientMethodAsync(id, "DoMath", 3, 5); 59 | await InvokeClientMethodOnlyAsync(id, "receiveMessage", "Server", $"You sent me this result: " + result); 60 | } 61 | catch (Exception ex) 62 | { 63 | await InvokeClientMethodOnlyAsync(id, "receiveMessage", "Server", $"I had an exception: " + ex.Message); 64 | } 65 | } 66 | 67 | public override async Task OnDisconnected(WebSocket socket) 68 | { 69 | var socketId = WebSocketConnectionManager.GetId(socket); 70 | 71 | await base.OnDisconnected(socket); 72 | 73 | var message = new Message() 74 | { 75 | MessageType = MessageType.Text, 76 | Data = $"{socketId} disconnected" 77 | }; 78 | await SendMessageToAllAsync(message); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /samples/ChatApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore; 4 | 5 | namespace ChatApplication 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | BuildWebHost(args).Run(); 12 | } 13 | 14 | public static IWebHost BuildWebHost(string[] args) 15 | { 16 | return WebHost.CreateDefaultBuilder(args) 17 | .UseStartup() 18 | .Build(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/ChatApplication/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:65109/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "ChatApplication": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:65110" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/ChatApplication/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | using WebSocketManager; 6 | 7 | namespace ChatApplication 8 | { 9 | public class Startup 10 | { 11 | public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider) 12 | { 13 | app.UseWebSockets(); 14 | app.MapWebSocketManager("/chat", serviceProvider.GetService()); 15 | 16 | app.UseStaticFiles(); 17 | } 18 | 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddWebSocketManager(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /samples/ChatApplication/wwwroot/WebSocketManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The WebSocketManager JavaScript Client. See https://github.com/radu-matei/websocket-manager/ for more information. 3 | */ 4 | var WebSocketManager = (function () { 5 | /** 6 | * Create a new web socket manager. 7 | * @param {any} url The web socket url (must start with ws://). 8 | */ 9 | var constructor = function (url) { 10 | if (url === undefined) console.error("WebSocketManager constructor requires valid 'url'."); 11 | _this = this; 12 | 13 | /** Collection of methods on this client. */ 14 | this.methods = []; 15 | 16 | /////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | /** 19 | * Create a new networking message. 20 | */ 21 | var Message = function (messageType, data) { 22 | this.$type = 'WebSocketManager.Common.Message'; 23 | this.messageType = messageType; 24 | this.data = data; 25 | }; 26 | /** Text message (constant: 0). */ 27 | Message.Text = 0; 28 | /** Remote method invocation request message (constant: 1). */ 29 | Message.MethodInvocation = 1; 30 | /** Connection event message (constant: 2). */ 31 | Message.ConnectionEvent = 2; 32 | /** Remote method return value message (constant: 3). */ 33 | Message.MethodReturnValue = 3; 34 | 35 | /////////////////////////////////////////////////////////////////////////////////////////// 36 | 37 | /** 38 | * Create a new invocation descriptor. 39 | * @param {any} methodName The name of the remote method. 40 | * @param {any} args The arguments passed to the method. 41 | * @param {any} identifier The unique identifier of the invocation. 42 | */ 43 | var InvocationDescriptor = function (methodName, args, identifier) { 44 | this.$type = 'WebSocketManager.Common.InvocationDescriptor'; 45 | this.methodName = methodName; 46 | this.arguments = { 47 | $type: 'System.Object[]', 48 | $values: args 49 | }; 50 | this.identifier = { 51 | $type: "System.Guid", 52 | $value: identifier 53 | }; 54 | }; 55 | 56 | /////////////////////////////////////////////////////////////////////////////////////////// 57 | 58 | /** 59 | * Represents the return value of a method that was executed remotely. 60 | * @param {any} identifier The unique identifier of the invocation. 61 | * @param {any} result The result of the method call. 62 | * @param {any} exception The remote exception of the method call. 63 | */ 64 | var InvocationResult = function (identifier, result, exception) { 65 | this.$type = 'WebSocketManager.Common.InvocationResult'; 66 | this.result = result; 67 | this.exception = exception; 68 | this.identifier = { 69 | $type: "System.Guid", 70 | $value: identifier 71 | }; 72 | if (exception !== undefined) { 73 | this.exception = { 74 | $type: "WebSocketManager.Common.RemoteException", 75 | message: exception 76 | } 77 | } 78 | }; 79 | 80 | /////////////////////////////////////////////////////////////////////////////////////////// 81 | 82 | /** 83 | * Collection of primitive type names and their C# mappings. 84 | */ 85 | var typemappings = { 86 | guid: 'System.Guid', 87 | uuid: 'System.Guid', // convenience alias 88 | bool: 'System.Boolean', 89 | byte: 'System.Byte', 90 | sbyte: 'System.SByte', 91 | char: 'System.Char', 92 | decimal: 'System.Decimal', 93 | double: 'System.Double', 94 | float: 'System.Single', 95 | int: 'System.Int32', 96 | uint: 'System.UInt32', 97 | long: 'System.Int64', 98 | ulong: 'System.UInt64', 99 | short: 'System.Int16', 100 | ushort: 'System.UInt16', 101 | string: 'System.String', 102 | object: 'System.Object' // generic 103 | }; 104 | 105 | /////////////////////////////////////////////////////////////////////////////////////////// 106 | 107 | /** 108 | * Generates a UUID using a random number generator and the current time. 109 | * This is not truly unique but it's good enough (TM). 110 | */ 111 | var uuid = function () { // Public Domain/MIT 112 | var d = new Date().getTime(); 113 | if (typeof performance !== 'undefined' && typeof performance.now === 'function') { 114 | d += performance.now(); // use high-precision timer if available 115 | } 116 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 117 | var r = (d + Math.random() * 16) % 16 | 0; 118 | d = Math.floor(d / 16); 119 | return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 120 | }); 121 | } 122 | 123 | /////////////////////////////////////////////////////////////////////////////////////////// 124 | 125 | /** 126 | * Takes a C# collection of $type+$value and turns them into a simple array of values. 127 | * @param {any} collection The C# collection of $type+$value. 128 | */ 129 | var parseCSharpArguments = function (collection) { 130 | var args = []; 131 | for (var i = 0; i < collection.length; i++) 132 | args.push(collection[i].$value); 133 | return args; 134 | } 135 | 136 | /////////////////////////////////////////////////////////////////////////////////////////// 137 | 138 | /** 139 | * The waiting remote invocations for Client to Server method calls (return values). 140 | */ 141 | var waitingRemoteInvocations = {}; 142 | 143 | /////////////////////////////////////////////////////////////////////////////////////////// 144 | 145 | /** 146 | * Called whenever the socket opens the connection. 147 | * @param {any} event The associated event data. 148 | */ 149 | var onSocketOpen = function (event) { 150 | }; 151 | 152 | /** 153 | * Called whenever the socket closes the connection. 154 | * @param {any} event The associated event data. 155 | */ 156 | var onSocketClose = function (event) { 157 | // public event: 158 | if (_this.onDisconnected !== undefined) _this.onDisconnected(); 159 | }; 160 | 161 | /** 162 | * Called whenever the socket has an error. 163 | * @param {any} event The associated event data. 164 | */ 165 | var onSocketError = function (event) { 166 | console.error("WebSocketManager error:"); 167 | console.error(event); 168 | }; 169 | 170 | /** 171 | * Called whenever there is an incoming message. 172 | * @param {any} message The Message that was received. 173 | */ 174 | var onSocketMessage = function (message) { 175 | // CONNECTION EVENT 176 | if (message.messageType === Message.ConnectionEvent) { 177 | // we received the unique identifier from the server. 178 | _this.id = message.data.$value; 179 | // public event: 180 | if (_this.onConnected !== undefined) _this.onConnected(_this.id); 181 | } 182 | 183 | // TEXT EVENT 184 | else if (message.messageType === Message.Text) { 185 | // public event: 186 | if (_this.onMessage !== undefined) _this.onMessage(message.data.$value); 187 | } 188 | 189 | // METHOD INVOCATION EVENT 190 | else if (message.messageType === Message.MethodInvocation) { 191 | var data = JSON.parse(message.data.$value); 192 | // find the method. 193 | if (_this.methods[data.methodName.$value] !== undefined) { 194 | // call the method and catch any exceptions. 195 | var result, error = undefined; 196 | try { result = _this.methods[data.methodName.$value].apply(_this, parseCSharpArguments(data.arguments['$values'])); } 197 | catch (e) { error = e; } 198 | 199 | // if the server desires a result we send a method return value. 200 | if (data.identifier.$value !== '00000000-0000-0000-0000-000000000000') { 201 | // an error occured so let the server know. 202 | if (error !== undefined) { 203 | // send web-socket message to the server. 204 | _this.socket.send(JSON.stringify(new Message(Message.MethodReturnValue, 205 | JSON.stringify(new InvocationResult(data.identifier.$value, null, "A remote exception occured: " + error)) 206 | ))); 207 | } 208 | // send result value to the server. 209 | else { 210 | // try finding an appropriate C# type. 211 | if (typemappings[result[0]] !== undefined) 212 | result[0] = typemappings[result[0]]; 213 | // send web-socket message to the server. 214 | _this.socket.send(JSON.stringify(new Message(Message.MethodReturnValue, 215 | JSON.stringify(new InvocationResult(data.identifier.$value, { $type: result[0], $value: result[1] })) 216 | ))); 217 | } 218 | } 219 | } else console.error("WebSocketManager: Server attempted to invoke unknown method '" + data.methodName.$value + "'!"); 220 | } 221 | 222 | // METHOD RETURN VALUE EVENT 223 | else if (message.messageType === Message.MethodReturnValue) { 224 | var data = JSON.parse(message.data.$value); 225 | // find the waiting remote invocation. 226 | var callback = waitingRemoteInvocations[data.identifier.$value]; 227 | // remove it from the waiting list. 228 | delete waitingRemoteInvocations[data.identifier.$value]; 229 | // call the callback. 230 | if (data.exception !== null) 231 | callback(undefined, data.exception.message.$value); 232 | else 233 | callback(data.result.$value, undefined); 234 | } 235 | 236 | //console.log(message); 237 | }; 238 | 239 | /////////////////////////////////////////////////////////////////////////////////////////// 240 | 241 | /** 242 | * Connects to the server. 243 | */ 244 | this.connect = function () { 245 | // create a new web-socket connection to the server. 246 | _this.socket = new WebSocket(url); 247 | 248 | _this.socket.onopen = function (event) { 249 | onSocketOpen(event); 250 | } 251 | 252 | _this.socket.onclose = function (event) { 253 | // run all the callbacks on the waiting list so the program continues. 254 | Object.keys(waitingRemoteInvocations).forEach(function (guid) { 255 | waitingRemoteInvocations[guid](undefined, 'The web-socket connection was closed.'); 256 | }); 257 | waitingRemoteInvocations = {}; 258 | 259 | onSocketClose(event); 260 | } 261 | 262 | _this.socket.onerror = function (event) { 263 | onSocketError(event); 264 | } 265 | 266 | _this.socket.onmessage = function (event) { 267 | onSocketMessage(JSON.parse(event.data)); 268 | } 269 | }; 270 | 271 | /** 272 | * Invoke a remote method on the server only, without a return value. 273 | * @param {any} method The name of the remote method to be invoked. 274 | */ 275 | this.invokeOnly = function (method) { 276 | var args = []; 277 | // iterate through all arguments and find type/value relationships. 278 | for (var i = 1; i < arguments.length; i += 2) { 279 | var type = arguments[i]; 280 | var value = arguments[i + 1]; 281 | // try finding an appropriate C# type. 282 | if (typemappings[type] !== undefined) 283 | type = typemappings[type]; 284 | // even if we can't find a C# type we assume the user knows what he's doing. 285 | args.push({ $type: type, $value: value }); 286 | } 287 | 288 | // send web-socket message to the server. 289 | _this.socket.send(JSON.stringify(new Message(Message.MethodInvocation, 290 | JSON.stringify(new InvocationDescriptor(method, args, '00000000-0000-0000-0000-000000000000')) 291 | ))); 292 | } 293 | 294 | /** 295 | * Invoke a remote method on the server, with a callback for the return value. 296 | * @param {any} method The name of the remote method to be invoked. 297 | */ 298 | this.invoke = function (method) { 299 | var args = []; 300 | // iterate through all arguments and find type/value relationships. 301 | for (var i = 1; i < arguments.length - 1; i += 2) { 302 | var type = arguments[i]; 303 | var value = arguments[i + 1]; 304 | // try finding an appropriate C# type. 305 | if (typemappings[type] !== undefined) 306 | type = typemappings[type]; 307 | // even if we can't find a C# type we assume the user knows what he's doing. 308 | args.push({ $type: type, $value: value }); 309 | } 310 | // the last argument should be the callback method. 311 | var callback = arguments[arguments.length - 1]; 312 | // generate a unique identifier to associate return values. 313 | var guid = uuid(); 314 | // put this call on the waiting list. 315 | waitingRemoteInvocations[guid] = callback; 316 | 317 | // send web-socket message to the server. 318 | _this.socket.send(JSON.stringify(new Message(Message.MethodInvocation, 319 | JSON.stringify(new InvocationDescriptor(method, args, guid)) 320 | ))); 321 | } 322 | }; 323 | 324 | return constructor; 325 | })(); -------------------------------------------------------------------------------- /samples/ChatApplication/wwwroot/client.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Test Page 7 | 8 | 9 | 10 |

This should be mapped to "/chat"

11 | 12 | 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /samples/EchoConsoleClient/EchoConsoleClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 0.2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/EchoConsoleClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using WebSocketManager.Client; 4 | using WebSocketManager.Common; 5 | 6 | public class Program 7 | { 8 | private static Connection _connection; 9 | private static StringMethodInvocationStrategy _strategy; 10 | 11 | public static void Main(string[] args) 12 | { 13 | StartConnectionAsync(); 14 | 15 | _strategy.On("receiveMessage", (arguments) => 16 | { 17 | Console.WriteLine($"{arguments[0]} said: {arguments[1]}"); 18 | }); 19 | 20 | Console.ReadLine(); 21 | StopConnectionAsync(); 22 | } 23 | 24 | public static async Task StartConnectionAsync() 25 | { 26 | _strategy = new StringMethodInvocationStrategy(); 27 | _connection = new Connection(_strategy); 28 | await _connection.StartConnectionAsync("ws://localhost:65110/chat"); 29 | } 30 | 31 | public static async Task StopConnectionAsync() 32 | { 33 | await _connection.StopConnectionAsync(); 34 | } 35 | } -------------------------------------------------------------------------------- /samples/MvcSample/Controllers/MessagesController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using MvcSample.MessageHandlers; 4 | 5 | namespace MvcSample.Controllers 6 | { 7 | public class MessagesController : Controller 8 | { 9 | private NotificationsMessageHandler _notificationsMessageHandler { get; set; } 10 | 11 | public MessagesController(NotificationsMessageHandler notificationsMessageHandler) 12 | { 13 | _notificationsMessageHandler = notificationsMessageHandler; 14 | } 15 | 16 | [HttpGet] 17 | public async Task SendMessage([FromQueryAttribute]string message) 18 | { 19 | await _notificationsMessageHandler.InvokeClientMethodToAllAsync("receiveMessage", message); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /samples/MvcSample/MessageHandlers/NotificationsMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using WebSocketManager; 2 | using WebSocketManager.Common; 3 | 4 | namespace MvcSample.MessageHandlers 5 | { 6 | public class NotificationsMessageHandler : WebSocketHandler 7 | { 8 | public NotificationsMessageHandler(WebSocketConnectionManager webSocketConnectionManager) : base(webSocketConnectionManager, new StringMethodInvocationStrategy()) 9 | { 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /samples/MvcSample/MvcSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | 0.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/MvcSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore; 3 | 4 | namespace MvcSample 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) 14 | { 15 | return WebHost.CreateDefaultBuilder(args) 16 | .UseStartup() 17 | .Build(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/MvcSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:65124/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "MvcSample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:65125" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/MvcSample/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MvcSample.MessageHandlers; 5 | using WebSocketManager; 6 | 7 | namespace MvcSample 8 | { 9 | public class Startup 10 | { 11 | public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider) 12 | { 13 | app.UseStaticFiles(); 14 | app.UseWebSockets(); 15 | 16 | app.UseMvc(routes => 17 | { 18 | routes.MapRoute( 19 | name: "default", 20 | template: "api/{controller}/{action}/{id?}" 21 | ); 22 | }); 23 | 24 | app.MapWebSocketManager("/notifications", serviceProvider.GetService()); 25 | } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddMvc(); 30 | services.AddWebSocketManager(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /samples/MvcSample/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Real-Time Notifications 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

    This should be mapped to "/notifications"

    17 | 18 | 19 | 20 |
      21 | 22 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /samples/WebSocketManagerSamples.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatApplication", "ChatApplication\ChatApplication.csproj", "{C64533FC-4066-438D-9297-BDF1150B3F9A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoConsoleClient", "EchoConsoleClient\EchoConsoleClient.csproj", "{DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcSample", "MvcSample\MvcSample.csproj", "{89A1D156-DE58-4C80-B8C3-B1B389764D95}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSocketManager", "..\src\WebSocketManager\WebSocketManager.csproj", "{9D92BAD3-058A-452C-8701-C0E7C3AE268E}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSocketManager.Common", "..\src\WebSocketManager.Common\WebSocketManager.Common.csproj", "{2A5BA455-1A0F-4B36-998D-0FBDBCF69137}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSocketManager.Client", "..\src\WebSocketManager.Client\WebSocketManager.Client.csproj", "{818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Debug|x64.ActiveCfg = Debug|Any CPU 31 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Debug|x64.Build.0 = Debug|Any CPU 32 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Debug|x86.ActiveCfg = Debug|Any CPU 33 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Debug|x86.Build.0 = Debug|Any CPU 34 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Release|x64.ActiveCfg = Release|Any CPU 37 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Release|x64.Build.0 = Release|Any CPU 38 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Release|x86.ActiveCfg = Release|Any CPU 39 | {C64533FC-4066-438D-9297-BDF1150B3F9A}.Release|x86.Build.0 = Release|Any CPU 40 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Debug|x64.ActiveCfg = Debug|Any CPU 43 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Debug|x64.Build.0 = Debug|Any CPU 44 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Debug|x86.Build.0 = Debug|Any CPU 46 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Release|x64.ActiveCfg = Release|Any CPU 49 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Release|x64.Build.0 = Release|Any CPU 50 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Release|x86.ActiveCfg = Release|Any CPU 51 | {DDE2DC69-2CD5-4C4F-98D3-3609942A6E63}.Release|x86.Build.0 = Release|Any CPU 52 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Debug|x64.ActiveCfg = Debug|Any CPU 55 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Debug|x64.Build.0 = Debug|Any CPU 56 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Debug|x86.ActiveCfg = Debug|Any CPU 57 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Debug|x86.Build.0 = Debug|Any CPU 58 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Release|x64.ActiveCfg = Release|Any CPU 61 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Release|x64.Build.0 = Release|Any CPU 62 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Release|x86.ActiveCfg = Release|Any CPU 63 | {89A1D156-DE58-4C80-B8C3-B1B389764D95}.Release|x86.Build.0 = Release|Any CPU 64 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Debug|x64.ActiveCfg = Debug|Any CPU 67 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Debug|x64.Build.0 = Debug|Any CPU 68 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Debug|x86.ActiveCfg = Debug|Any CPU 69 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Debug|x86.Build.0 = Debug|Any CPU 70 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Release|x64.ActiveCfg = Release|Any CPU 73 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Release|x64.Build.0 = Release|Any CPU 74 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Release|x86.ActiveCfg = Release|Any CPU 75 | {9D92BAD3-058A-452C-8701-C0E7C3AE268E}.Release|x86.Build.0 = Release|Any CPU 76 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Debug|x64.ActiveCfg = Debug|Any CPU 79 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Debug|x64.Build.0 = Debug|Any CPU 80 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Debug|x86.ActiveCfg = Debug|Any CPU 81 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Debug|x86.Build.0 = Debug|Any CPU 82 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Release|x64.ActiveCfg = Release|Any CPU 85 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Release|x64.Build.0 = Release|Any CPU 86 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Release|x86.ActiveCfg = Release|Any CPU 87 | {2A5BA455-1A0F-4B36-998D-0FBDBCF69137}.Release|x86.Build.0 = Release|Any CPU 88 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 89 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 90 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Debug|x64.ActiveCfg = Debug|Any CPU 91 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Debug|x64.Build.0 = Debug|Any CPU 92 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Debug|x86.ActiveCfg = Debug|Any CPU 93 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Debug|x86.Build.0 = Debug|Any CPU 94 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Release|Any CPU.Build.0 = Release|Any CPU 96 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Release|x64.ActiveCfg = Release|Any CPU 97 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Release|x64.Build.0 = Release|Any CPU 98 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Release|x86.ActiveCfg = Release|Any CPU 99 | {818BF6BF-9F97-47E5-8AFC-77E3EDA2A5C9}.Release|x86.Build.0 = Release|Any CPU 100 | EndGlobalSection 101 | GlobalSection(SolutionProperties) = preSolution 102 | HideSolutionNode = FALSE 103 | EndGlobalSection 104 | GlobalSection(ExtensibilityGlobals) = postSolution 105 | SolutionGuid = {792302F2-9E03-46AD-B5B7-F3391E84E1D3} 106 | EndGlobalSection 107 | GlobalSection(MonoDevelopProperties) = preSolution 108 | version = 0.2 109 | EndGlobalSection 110 | EndGlobal 111 | -------------------------------------------------------------------------------- /samples/WebTerm/ConsoleAppManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace WebTerm 9 | { 10 | // taken from http://stackoverflow.com/questions/21848271/redirecting-standard-input-of-console-application and slightly modified 11 | public class ConsoleAppManager 12 | { 13 | private readonly string appName; 14 | private readonly Process process = new Process(); 15 | private readonly object theLock = new object(); 16 | private SynchronizationContext context; 17 | private string pendingWriteData; 18 | 19 | public ConsoleAppManager(string appName) 20 | { 21 | this.appName = appName; 22 | 23 | this.process.StartInfo.FileName = this.appName; 24 | this.process.StartInfo.RedirectStandardError = true; 25 | this.process.StartInfo.StandardErrorEncoding = Encoding.UTF8; 26 | 27 | this.process.StartInfo.RedirectStandardInput = true; 28 | this.process.StartInfo.RedirectStandardOutput = true; 29 | this.process.EnableRaisingEvents = true; 30 | this.process.StartInfo.CreateNoWindow = true; 31 | 32 | this.process.StartInfo.UseShellExecute = false; 33 | 34 | this.process.StartInfo.StandardOutputEncoding = Encoding.UTF8; 35 | 36 | this.process.Exited += this.ProcessOnExited; 37 | } 38 | 39 | public event EventHandler ErrorTextReceived; 40 | public event EventHandler ProcessExited; 41 | public event EventHandler StandartTextReceived; 42 | 43 | public int ExitCode 44 | { 45 | get { return this.process.ExitCode; } 46 | } 47 | 48 | public bool Running 49 | { 50 | get; private set; 51 | } 52 | 53 | public void ExecuteAsync(params string[] args) 54 | { 55 | if (this.Running) 56 | { 57 | throw new InvalidOperationException( 58 | "Process is still Running. Please wait for the process to complete."); 59 | } 60 | 61 | string arguments = string.Join(" ", args); 62 | 63 | this.process.StartInfo.Arguments = arguments; 64 | 65 | this.context = SynchronizationContext.Current; 66 | 67 | this.process.Start(); 68 | this.Running = true; 69 | 70 | new Task(this.ReadOutputAsync).Start(); 71 | new Task(this.WriteInputTask).Start(); 72 | new Task(this.ReadOutputErrorAsync).Start(); 73 | } 74 | 75 | public void Write(string data) 76 | { 77 | if (data == null) 78 | { 79 | return; 80 | } 81 | 82 | lock (this.theLock) 83 | { 84 | this.pendingWriteData = data; 85 | } 86 | } 87 | 88 | public void WriteLine(string data) 89 | { 90 | this.Write(data + Environment.NewLine); 91 | } 92 | 93 | protected virtual void OnErrorTextReceived(string e) 94 | { 95 | EventHandler handler = this.ErrorTextReceived; 96 | 97 | if (handler != null) 98 | { 99 | if (this.context != null) 100 | { 101 | this.context.Post(delegate { handler(this, e); }, null); 102 | } 103 | else 104 | { 105 | handler(this, e); 106 | } 107 | } 108 | } 109 | 110 | protected virtual void OnProcessExited() 111 | { 112 | EventHandler handler = this.ProcessExited; 113 | if (handler != null) 114 | { 115 | handler(this, EventArgs.Empty); 116 | } 117 | } 118 | 119 | protected virtual void OnStandartTextReceived(string e) 120 | { 121 | EventHandler handler = this.StandartTextReceived; 122 | 123 | if (handler != null) 124 | { 125 | if (this.context != null) 126 | { 127 | this.context.Post(delegate { handler(this, e); }, null); 128 | } 129 | else 130 | { 131 | handler(this, e); 132 | } 133 | } 134 | } 135 | 136 | private void ProcessOnExited(object sender, EventArgs eventArgs) 137 | { 138 | this.OnProcessExited(); 139 | } 140 | 141 | private async void ReadOutputAsync() 142 | { 143 | var standart = new StringBuilder(); 144 | var buff = new char[1024]; 145 | int length; 146 | 147 | while (this.process.HasExited == false) 148 | { 149 | standart.Clear(); 150 | 151 | length = await this.process.StandardOutput.ReadAsync(buff, 0, buff.Length); 152 | standart.Append(buff.SubArray(0, length)); 153 | this.OnStandartTextReceived(standart.ToString()); 154 | Thread.Sleep(1); 155 | } 156 | 157 | this.Running = false; 158 | } 159 | 160 | private async void ReadOutputErrorAsync() 161 | { 162 | var sb = new StringBuilder(); 163 | 164 | do 165 | { 166 | sb.Clear(); 167 | var buff = new char[1024]; 168 | int length = await this.process.StandardError.ReadAsync(buff, 0, buff.Length); 169 | sb.Append(buff.SubArray(0, length)); 170 | this.OnErrorTextReceived(sb.ToString()); 171 | Thread.Sleep(1); 172 | } 173 | while (this.process.HasExited == false); 174 | } 175 | 176 | private async void WriteInputTask() 177 | { 178 | while (this.process.HasExited == false) 179 | { 180 | Thread.Sleep(1); 181 | 182 | if (this.pendingWriteData != null) 183 | { 184 | await this.process.StandardInput.WriteAsync(this.pendingWriteData); 185 | await this.process.StandardInput.FlushAsync(); 186 | 187 | lock (this.theLock) 188 | { 189 | this.pendingWriteData = null; 190 | } 191 | } 192 | } 193 | } 194 | 195 | public void Kill() { 196 | this.process.Kill(); 197 | } 198 | } 199 | 200 | public static class CharArrayExtensions 201 | { 202 | public static char[] SubArray(this char[] input, int startIndex, int length) 203 | { 204 | List result = new List(); 205 | for (int i = startIndex; i < length; i++) 206 | { 207 | result.Add(input[i]); 208 | } 209 | 210 | return result.ToArray(); 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /samples/WebTerm/NOTICE: -------------------------------------------------------------------------------- 1 | Webterm Project 2 | 3 | Webterm includes works distributed under the licenses listed below. The full text for most of the licenses listed below can be found in the LICENSE.txt file accompanying each work. The original copyright notices have been preserved within the respective files and or packages. Please refer to the specific files and/or packages for more detailed information about the authors, copyright notices, and licenses. 4 | 5 | Kudu 6 | ----- 7 | Website: https://github.com/projectkudu/kudu/ 8 | Copyright: Copyright 2015 .NET Foundation 9 | License: Apache 2.0 10 | Modifications:webterm.js is a modified version of KuduExecV2.js 11 | 12 | jQuery 13 | ----- 14 | Website: http://jquery.com 15 | Copyright: Copyright (c) 2010 John Resig, http://jquery.com 16 | License: MIT 17 | 18 | jQuery jquery-console 19 | ----- 20 | Website: https://github.com/chrisdone/jquery-console 21 | Copyright: Copyright 2010 Chris Done, Simon David Pratt. All rights reserved. 22 | License: http://github.com/chrisdone/jquery-console/blob/master/README.md 23 | 24 | Json.NET 25 | ----- 26 | Website: http://json.codeplex.com/ 27 | Copyright: Copyright (c) 2007 James Newton-King 28 | License: MIT -------------------------------------------------------------------------------- /samples/WebTerm/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | namespace WebTerm { 6 | public class Program { 7 | public static void Main(string[] args) { 8 | var host = new WebHostBuilder() 9 | .UseKestrel() 10 | .UseContentRoot(Directory.GetCurrentDirectory()) 11 | .UseStartup() 12 | .Build(); 13 | 14 | host.Run(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/WebTerm/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | using WebSocketManager; 6 | 7 | namespace WebTerm { 8 | public class Startup { 9 | public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider) { 10 | app.UseWebSockets(); 11 | app.MapWebSocketManager("/cmd", serviceProvider.GetService()); 12 | 13 | app.UseStaticFiles(); 14 | } 15 | 16 | public void ConfigureServices(IServiceCollection services) { 17 | services.AddWebSocketManager(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/WebTerm/WebTermHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.WebSockets; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using WebSocketManager; 7 | using WebSocketManager.Common; 8 | 9 | namespace WebTerm { 10 | public class WebTermHandler : WebSocketHandler { 11 | private Dictionary socketIdToProcess = new Dictionary(); 12 | private Dictionary processToSocketId = new Dictionary(); 13 | 14 | public WebTermHandler(WebSocketConnectionManager webSocketConnectionManager) 15 | : base(webSocketConnectionManager) { 16 | } 17 | 18 | private void Mgr_ErrorTextReceived(object sender, string e) { 19 | Console.WriteLine("e>>>" + e + "<<>>exit<<<"); 28 | } 29 | 30 | private void Mgr_StandartTextReceived(object sender, string e) { 31 | Console.WriteLine(">>>" + e + "<<<"); 32 | string socketId; 33 | if (processToSocketId.TryGetValue(sender as ConsoleAppManager, out socketId)) { 34 | SendToClient(socketId, e, null).Wait(); 35 | } 36 | } 37 | 38 | public override async Task OnConnected(WebSocket socket) { 39 | await base.OnConnected(socket); 40 | 41 | var socketId = WebSocketConnectionManager.GetId(socket); 42 | 43 | var message = new Message() { 44 | MessageType = MessageType.Text, 45 | Data = "{ \"Output\": \"connected\", \"Error\": \"\" }" 46 | }; 47 | 48 | System.Console.WriteLine("new connection"); 49 | 50 | var mgr = new ConsoleAppManager("cmd.exe"); 51 | socketIdToProcess[socketId] = mgr; 52 | processToSocketId[mgr] = socketId; 53 | mgr.StandartTextReceived += Mgr_StandartTextReceived; 54 | mgr.ErrorTextReceived += Mgr_ErrorTextReceived; 55 | mgr.ProcessExited += Mgr_ProcessExited; 56 | 57 | mgr.ExecuteAsync("/Q"); 58 | 59 | await SendMessageToAllAsync(message); 60 | } 61 | 62 | public async Task ReceiveMessage(string socketId, string input) { 63 | System.Console.WriteLine($"ReceiveMessage ({socketId}): {input}"); 64 | socketIdToProcess[socketId].Write(input); 65 | } 66 | 67 | public async Task SendToClient(string socketId, string output, string error) { 68 | System.Console.WriteLine($"SendToClient ({socketId}): {output}"); 69 | await InvokeClientMethodAsync(socketId, "receiveMessage", new object[] { 70 | new { 71 | Output = output, 72 | Error = error 73 | }} 74 | ); 75 | } 76 | 77 | public override async Task OnDisconnected(WebSocket socket) { 78 | var socketId = WebSocketConnectionManager.GetId(socket); 79 | 80 | await base.OnDisconnected(socket); 81 | 82 | var message = new Message() { 83 | MessageType = MessageType.Text, 84 | Data = "{'Output': 'disconnected', 'Error': ''}" 85 | }; 86 | 87 | var mgr = socketIdToProcess[socketId]; 88 | mgr.Kill(); 89 | processToSocketId.Remove(mgr); 90 | socketIdToProcess.Remove(socketId); 91 | 92 | System.Console.WriteLine("disconnected"); 93 | await SendMessageToAllAsync(message); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /samples/WebTerm/wwwroot/FileBrowser.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | #webtermConsole { 8 | height: 80%; 9 | width: 97%; 10 | } 11 | 12 | div.console { 13 | font-family: Consolas, 'Lucida Console', 'Courier New', monospace; 14 | height: 100%; 15 | margin: auto; 16 | white-space: pre-wrap; 17 | width: 100%; 18 | } 19 | 20 | div.console div.jquery-console-inner { 21 | background-color: black; 22 | color: white; 23 | height: 100%; 24 | margin-left: auto; 25 | margin-right: auto; 26 | overflow: auto; 27 | word-break: break-all; 28 | word-wrap: break-word; 29 | padding: 0.3em; 30 | width: 100%; 31 | } 32 | 33 | div.console div.jquery-console-focus span.jquery-console-cursor { 34 | background: #5bc0de; 35 | color: black; 36 | animation: blink 1s steps(5, start) infinite; 37 | -webkit-animation: blink 1s steps(5, start) infinite; 38 | } 39 | 40 | @-webkit-keyframes blink { 41 | to { 42 | visibility: hidden; 43 | } 44 | } 45 | 46 | @keyframes blink { 47 | to { 48 | visibility: hidden; 49 | } 50 | } 51 | 52 | div.console div.jquery-console-message-error { 53 | color: red; 54 | } 55 | 56 | div.console div.jquery-console-message-value { 57 | color: white; 58 | } 59 | 60 | div.console div.jquery-console-message-type { 61 | color: white; 62 | } 63 | 64 | div.console span.jquery-console-prompt-pid { 65 | color: green; 66 | } 67 | -------------------------------------------------------------------------------- /samples/WebTerm/wwwroot/Index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

      Webterm

      17 |
      18 | 19 | -------------------------------------------------------------------------------- /samples/WebTerm/wwwroot/jquery.console.js: -------------------------------------------------------------------------------- 1 | // JQuery Console 1.0 2 | // Sun Feb 21 20:28:47 GMT 2010 3 | // 4 | // Copyright 2010 Chris Done, Simon David Pratt. All rights reserved. 5 | // 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions 8 | // are met: 9 | // 10 | // 1. Redistributions of source code must retain the above 11 | // copyright notice, this list of conditions and the following 12 | // disclaimer. 13 | // 14 | // 2. Redistributions in binary form must reproduce the above 15 | // copyright notice, this list of conditions and the following 16 | // disclaimer in the documentation and/or other materials 17 | // provided with the distribution. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 22 | // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 23 | // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 24 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 28 | // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 29 | // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | // POSSIBILITY OF SUCH DAMAGE. 31 | 32 | // TESTED ON 33 | // Internet Explorer 6 34 | // Opera 10.01 35 | // Chromium 4.0.237.0 (Ubuntu build 31094) 36 | // Firefox 3.5.8, 3.6.2 (Mac) 37 | // Safari 4.0.5 (6531.22.7) (Mac) 38 | // Google Chrome 5.0.375.55 (Mac) 39 | 40 | (function($){ 41 | var isWebkit = !!~navigator.userAgent.indexOf(' AppleWebKit/'); 42 | 43 | $.fn.console = function(config){ 44 | //////////////////////////////////////////////////////////////////////// 45 | // Constants 46 | // Some are enums, data types, others just for optimization 47 | var keyCodes = { 48 | // left 49 | 37: moveBackward, 50 | // right 51 | 39: moveForward, 52 | // up 53 | 38: previousHistory, 54 | // down 55 | 40: nextHistory, 56 | // backspace 57 | 8: backDelete, 58 | // delete 59 | 46: forwardDelete, 60 | // end 61 | 35: moveToEnd, 62 | // start 63 | 36: moveToStart, 64 | // return 65 | 13: commandTrigger, 66 | // tab 67 | 18: doNothing, 68 | // tab 69 | 9: doComplete, 70 | // escape 71 | 27: deleteFromStart, 72 | }; 73 | var ctrlCodes = { 74 | // C-a 75 | 65: moveToStart, 76 | // C-e 77 | 69: moveToEnd, 78 | // C-d 79 | 68: forwardDelete, 80 | // C-n 81 | 78: nextHistory, 82 | // C-p 83 | 80: previousHistory, 84 | // C-b 85 | 66: moveBackward, 86 | // C-f 87 | 70: moveForward, 88 | // C-k 89 | 75: deleteUntilEnd 90 | }; 91 | var altCodes = { 92 | // M-f 93 | 70: moveToNextWord, 94 | // M-b 95 | 66: moveToPreviousWord, 96 | // M-d 97 | 68: deleteNextWord 98 | }; 99 | var cursor = ' '; 100 | 101 | //////////////////////////////////////////////////////////////////////// 102 | // Globals 103 | var container = $(this); 104 | var inner = $('
      '); 105 | // erjiang: changed this from a text input to a textarea so we 106 | // can get pasted newlines 107 | var typer = $(''); 108 | // Prompt 109 | var promptBox; 110 | var prompt; 111 | var promptLabel = config && config.promptLabel? config.promptLabel : "> "; 112 | var continuedPromptLabel = config && config.continuedPromptLabel? 113 | config.continuedPromptLabel : "> "; 114 | var column = 0; 115 | var promptText = ''; 116 | var restoreText = ''; 117 | var continuedText = ''; 118 | // Prompt history stack 119 | var history = []; 120 | var ringn = 0; 121 | // For reasons unknown to The Sword of Michael himself, Opera 122 | // triggers and sends a key character when you hit various 123 | // keys like PgUp, End, etc. So there is no way of knowing 124 | // when a user has typed '#' or End. My solution is in the 125 | // typer.keydown and typer.keypress functions; I use the 126 | // variable below to ignore the keypress event if the keydown 127 | // event succeeds. 128 | var cancelKeyPress = 0; 129 | // When this value is false, the prompt will not respond to input 130 | var acceptInput = true; 131 | // When this value is true, the command has been canceled 132 | var cancelCommand = false; 133 | 134 | // External exports object 135 | var extern = {}; 136 | 137 | //////////////////////////////////////////////////////////////////////// 138 | // Main entry point 139 | (function(){ 140 | container.append(inner); 141 | inner.append(typer); 142 | typer.css({position:'absolute',top:0,left:'-9999px'}); 143 | if (config.welcomeMessage) 144 | message(config.welcomeMessage,'jquery-console-welcome'); 145 | newPromptBox(); 146 | if (config.autofocus) { 147 | inner.addClass('jquery-console-focus'); 148 | typer.focus(); 149 | setTimeout(function(){ 150 | inner.addClass('jquery-console-focus'); 151 | typer.focus(); 152 | },100); 153 | } 154 | extern.inner = inner; 155 | extern.typer = typer; 156 | extern.scrollToBottom = scrollToBottom; 157 | })(); 158 | 159 | //////////////////////////////////////////////////////////////////////// 160 | // Reset terminal 161 | extern.reset = function(){ 162 | var welcome = (typeof config.welcomeMessage != 'undefined'); 163 | inner.find('div').remove(); 164 | newPromptBox(); 165 | inner.addClass('jquery-console-focus'); 166 | typer.focus(); 167 | }; 168 | 169 | //////////////////////////////////////////////////////////////////////// 170 | // Reset terminal 171 | extern.notice = function(msg,style){ 172 | var n = $('
      ').append($('
      ').text(msg)) 173 | .css({visibility:'hidden'}); 174 | container.append(n); 175 | var focused = true; 176 | if (style=='fadeout') 177 | setTimeout(function(){ 178 | n.fadeOut(function(){ 179 | n.remove(); 180 | }); 181 | },4000); 182 | else if (style=='prompt') { 183 | var a = $('
      '); 184 | n.append(a); 185 | focused = false; 186 | a.click(function(){ n.fadeOut(function(){ n.remove();inner.css({opacity:1}) }); }); 187 | } 188 | var h = n.height(); 189 | n.css({height:'0px',visibility:'visible'}) 190 | .animate({height:h+'px'},function(){ 191 | if (!focused) inner.css({opacity:0.5}); 192 | }); 193 | n.css('cursor','default'); 194 | return n; 195 | }; 196 | 197 | //////////////////////////////////////////////////////////////////////// 198 | // Reset history 199 | extern.resetHistory = function () { 200 | ringn = 0; 201 | }; 202 | 203 | //////////////////////////////////////////////////////////////////////// 204 | // Compute a promptLabel 205 | function getPromptLabel() { 206 | var promptLabelText; 207 | if (typeof promptLabel == 'function') { 208 | promptLabelText = promptLabel(); 209 | } else { 210 | promptLabelText = promptLabel; 211 | } 212 | return extern.continuedPrompt ? continuedPromptLabel : promptLabelText; 213 | } 214 | 215 | //////////////////////////////////////////////////////////////////////// 216 | // Make a new prompt box 217 | function newPromptBox() { 218 | column = 0; 219 | promptText = ''; 220 | ringn = 0; // Reset the position of the history ring 221 | enableInput(); 222 | promptBox = $('
      '); 223 | var label = $(''); 224 | var pid = $(''); 225 | var labelText = getPromptLabel(); 226 | promptBox.append(pid.text("").show()); 227 | promptBox.append(label.text(labelText).show()); 228 | label.html(label.html().replace(' ',' ')); 229 | prompt = $(''); 230 | promptBox.append(prompt); 231 | inner.append(promptBox); 232 | updatePromptDisplay(); 233 | }; 234 | 235 | //////////////////////////////////////////////////////////////////////// 236 | // Handle setting focus 237 | container.click(function(){ 238 | // Don't mess with the focus if there is an active selection 239 | if (window.getSelection().toString()) { 240 | return false; 241 | } 242 | 243 | inner.addClass('jquery-console-focus'); 244 | inner.removeClass('jquery-console-nofocus'); 245 | if (isWebkit) { 246 | typer.focusWithoutScrolling(); 247 | } else { 248 | typer.css('position', 'fixed').focus(); 249 | } 250 | scrollToBottom(); 251 | return false; 252 | }); 253 | 254 | //////////////////////////////////////////////////////////////////////// 255 | // Handle losing focus 256 | typer.blur(function(){ 257 | inner.removeClass('jquery-console-focus'); 258 | inner.addClass('jquery-console-nofocus'); 259 | }); 260 | 261 | //////////////////////////////////////////////////////////////////////// 262 | // Bind to the paste event of the input box so we know when we 263 | // get pasted data 264 | typer.bind('paste', function(e) { 265 | // wipe typer input clean just in case 266 | typer.val(""); 267 | // this timeout is required because the onpaste event is 268 | // fired *before* the text is actually pasted 269 | setTimeout(function() { 270 | typer.consoleInsert(typer.val()); 271 | typer.val(""); 272 | }, 0); 273 | }); 274 | 275 | //////////////////////////////////////////////////////////////////////// 276 | // Handle key hit before translation 277 | // For picking up control characters like up/left/down/right 278 | 279 | typer.keydown(function(e){ 280 | cancelKeyPress = 0; 281 | var keyCode = e.keyCode; 282 | // C-c: cancel the execution 283 | if(e.ctrlKey && keyCode == 67) { 284 | cancelKeyPress = keyCode; 285 | cancelExecution(); 286 | return false; 287 | } 288 | if (acceptInput) { 289 | if (typeof config.userInputHandle == 'function') { 290 | config.userInputHandle(keyCode); 291 | } 292 | if (keyCode in keyCodes) { 293 | cancelKeyPress = keyCode; 294 | (keyCodes[keyCode])(e); 295 | return false; 296 | } else if (e.ctrlKey && keyCode in ctrlCodes) { 297 | cancelKeyPress = keyCode; 298 | (ctrlCodes[keyCode])(); 299 | return false; 300 | } else if (e.altKey && keyCode in altCodes) { 301 | cancelKeyPress = keyCode; 302 | (altCodes[keyCode])(); 303 | return false; 304 | } 305 | } 306 | }); 307 | 308 | //////////////////////////////////////////////////////////////////////// 309 | // Handle key press 310 | typer.keypress(function(e){ 311 | var keyCode = e.keyCode || e.which; 312 | if (isIgnorableKey(e)) { 313 | return false; 314 | } 315 | // C-v: don't insert on paste event 316 | if ((e.ctrlKey || e.metaKey) && String.fromCharCode(keyCode).toLowerCase() == 'v') { 317 | return true; 318 | } 319 | if (acceptInput && cancelKeyPress != keyCode && keyCode >= 32){ 320 | if (cancelKeyPress) return false; 321 | if ( 322 | typeof config.charInsertTrigger == 'undefined' || ( 323 | typeof config.charInsertTrigger == 'function' && 324 | config.charInsertTrigger(keyCode,promptText) 325 | ) 326 | ){ 327 | typer.consoleInsert(keyCode); 328 | } 329 | } 330 | if (isWebkit) return false; 331 | }); 332 | 333 | function isIgnorableKey(e) { 334 | // for now just filter alt+tab that we receive on some platforms when 335 | // user switches windows (goes away from the browser) 336 | return ((e.keyCode == keyCodes.tab || e.keyCode == 192) && e.altKey); 337 | }; 338 | 339 | //////////////////////////////////////////////////////////////////////// 340 | // Rotate through the command history 341 | function rotateHistory(n){ 342 | if (history.length == 0) return; 343 | ringn += n; 344 | if (ringn < 0) ringn = history.length; 345 | else if (ringn > history.length) ringn = 0; 346 | var prevText = promptText; 347 | if (ringn == 0) { 348 | promptText = restoreText; 349 | } else { 350 | promptText = history[ringn - 1]; 351 | } 352 | if (config.historyPreserveColumn) { 353 | if (promptText.length < column + 1) { 354 | column = promptText.length; 355 | } else if (column == 0) { 356 | column = promptText.length; 357 | } 358 | } else { 359 | column = promptText.length; 360 | } 361 | updatePromptDisplay(); 362 | }; 363 | 364 | function previousHistory() { 365 | rotateHistory(-1); 366 | }; 367 | 368 | function nextHistory() { 369 | rotateHistory(1); 370 | }; 371 | 372 | // Add something to the history ring 373 | function addToHistory(line) { 374 | if (!line || line.trim() == "") 375 | return; 376 | history.push(line); 377 | restoreText = ''; 378 | }; 379 | 380 | // Delete the character at the current position 381 | function deleteCharAtPos(){ 382 | if (column < promptText.length){ 383 | promptText = 384 | promptText.substring(0,column) + 385 | promptText.substring(column+1); 386 | restoreText = promptText; 387 | return true; 388 | } else return false; 389 | }; 390 | 391 | function backDelete() { 392 | if (moveColumn(-1)){ 393 | deleteCharAtPos(); 394 | updatePromptDisplay(); 395 | } 396 | }; 397 | 398 | function forwardDelete() { 399 | if (deleteCharAtPos()){ 400 | updatePromptDisplay(); 401 | } 402 | }; 403 | 404 | function deleteUntilEnd() { 405 | while(deleteCharAtPos()) { 406 | updatePromptDisplay(); 407 | } 408 | }; 409 | 410 | function deleteFromStart() { 411 | if (moveColumn(-column)) { 412 | while (deleteCharAtPos()); 413 | updatePromptDisplay(); 414 | } 415 | } 416 | 417 | function deleteNextWord() { 418 | // A word is defined within this context as a series of alphanumeric 419 | // characters. 420 | // Delete up to the next alphanumeric character 421 | while( 422 | column < promptText.length && 423 | !isCharAlphanumeric(promptText[column]) 424 | ) { 425 | deleteCharAtPos(); 426 | updatePromptDisplay(); 427 | } 428 | // Then, delete until the next non-alphanumeric character 429 | while( 430 | column < promptText.length && 431 | isCharAlphanumeric(promptText[column]) 432 | ) { 433 | deleteCharAtPos(); 434 | updatePromptDisplay(); 435 | } 436 | }; 437 | 438 | //////////////////////////////////////////////////////////////////////// 439 | // Validate command and trigger it if valid, or show a validation error 440 | function commandTrigger() { 441 | var line = promptText; 442 | if (typeof config.commandValidate == 'function') { 443 | var ret = config.commandValidate(line); 444 | if (ret == true || ret == false) { 445 | if (ret) { 446 | handleCommand(); 447 | } 448 | } else { 449 | commandResult(ret,"jquery-console-message-error"); 450 | } 451 | } else { 452 | handleCommand(); 453 | } 454 | }; 455 | 456 | // Scroll to the bottom of the view 457 | function scrollToBottom() { 458 | if (jQuery.fn.jquery > "1.6") { 459 | inner.prop({ scrollTop: inner.prop("scrollHeight") }); 460 | } 461 | else { 462 | inner.attr({ scrollTop: inner.attr("scrollHeight") }); 463 | } 464 | }; 465 | 466 | function cancelExecution() { 467 | if(typeof config.cancelHandle == 'function') { 468 | config.cancelHandle(); 469 | } 470 | } 471 | 472 | //////////////////////////////////////////////////////////////////////// 473 | // Handle a command 474 | function handleCommand() { 475 | if (typeof config.commandHandle == 'function') { 476 | disableInput(); 477 | addToHistory(promptText); 478 | var text = promptText; 479 | if (extern.continuedPrompt) { 480 | if (continuedText) 481 | continuedText += '\n' + promptText; 482 | else continuedText = promptText; 483 | } else continuedText = undefined; 484 | if (continuedText) text = continuedText; 485 | var ret = config.commandHandle(text,function(msgs){ 486 | commandResult(msgs); 487 | }); 488 | if (extern.continuedPrompt && !continuedText) 489 | continuedText = promptText; 490 | if (typeof ret == 'boolean') { 491 | if (ret) { 492 | // Command succeeded without a result. 493 | commandResult(); 494 | } else { 495 | commandResult( 496 | 'Command failed.', 497 | "jquery-console-message-error" 498 | ); 499 | } 500 | } else if (typeof ret == "string") { 501 | commandResult(ret,"jquery-console-message-success"); 502 | } else if (typeof ret == 'object' && ret.length) { 503 | commandResult(ret); 504 | } else if (extern.continuedPrompt) { 505 | commandResult(); 506 | } 507 | } 508 | }; 509 | 510 | //////////////////////////////////////////////////////////////////////// 511 | // Disable input 512 | function disableInput() { 513 | acceptInput = false; 514 | }; 515 | 516 | // Enable input 517 | function enableInput() { 518 | acceptInput = true; 519 | } 520 | 521 | //////////////////////////////////////////////////////////////////////// 522 | // Reset the prompt in invalid command 523 | function commandResult(msg,className,ignorePrompt) { 524 | column = -1; 525 | if (!ignorePrompt) { 526 | updatePromptDisplay(); 527 | } 528 | if (typeof msg == 'string') { 529 | message(msg,className); 530 | } else if ($.isArray(msg)) { 531 | for (var x in msg) { 532 | var ret = msg[x]; 533 | message(ret.msg,ret.className); 534 | } 535 | } else { // Assume it's a DOM node or jQuery object. 536 | inner.append(msg); 537 | } 538 | if (!ignorePrompt) { 539 | newPromptBox(); 540 | } 541 | }; 542 | 543 | //////////////////////////////////////////////////////////////////////// 544 | // Display a message 545 | function message(msg,className) { 546 | var mesg = $('
      '); 547 | if (className) mesg.addClass(className); 548 | mesg.filledText(msg).hide(); 549 | inner.append(mesg); 550 | mesg.show(); 551 | return mesg; 552 | }; 553 | 554 | extern.message = message; 555 | extern.enableInput = enableInput; 556 | 557 | //////////////////////////////////////////////////////////////////////// 558 | // Handle normal character insertion 559 | // data can either be a number, which will be interpreted as the 560 | // numeric value of a single character, or a string 561 | typer.consoleInsert = function(data){ 562 | // TODO: remove redundant indirection 563 | var text = isNaN(data) ? data : String.fromCharCode(data); 564 | var before = promptText.substring(0,column); 565 | var after = promptText.substring(column); 566 | promptText = before + text + after; 567 | moveColumn(text.length); 568 | restoreText = promptText; 569 | updatePromptDisplay(); 570 | }; 571 | 572 | //////////////////////////////////////////////////////////////////////// 573 | // Move to another column relative to this one 574 | // Negative means go back, positive means go forward. 575 | function moveColumn(n){ 576 | if (column + n >= 0 && column + n <= promptText.length){ 577 | column += n; 578 | return true; 579 | } else return false; 580 | }; 581 | 582 | function moveForward() { 583 | if(moveColumn(1)) { 584 | updatePromptDisplay(); 585 | return true; 586 | } 587 | return false; 588 | }; 589 | 590 | function moveBackward() { 591 | if(moveColumn(-1)) { 592 | updatePromptDisplay(); 593 | return true; 594 | } 595 | return false; 596 | }; 597 | 598 | function moveToStart() { 599 | if (moveColumn(-column)) 600 | updatePromptDisplay(); 601 | }; 602 | 603 | function moveToEnd() { 604 | if (moveColumn(promptText.length-column)) 605 | updatePromptDisplay(); 606 | }; 607 | 608 | function moveToNextWord() { 609 | while( 610 | column < promptText.length && 611 | !isCharAlphanumeric(promptText[column]) && 612 | moveForward() 613 | ) {} 614 | while( 615 | column < promptText.length && 616 | isCharAlphanumeric(promptText[column]) && 617 | moveForward() 618 | ) {} 619 | }; 620 | 621 | function moveToPreviousWord() { 622 | // Move backward until we find the first alphanumeric 623 | while( 624 | column -1 >= 0 && 625 | !isCharAlphanumeric(promptText[column-1]) && 626 | moveBackward() 627 | ) {} 628 | // Move until we find the first non-alphanumeric 629 | while( 630 | column -1 >= 0 && 631 | isCharAlphanumeric(promptText[column-1]) && 632 | moveBackward() 633 | ) {} 634 | }; 635 | 636 | function isCharAlphanumeric(charToTest) { 637 | if(typeof charToTest == 'string') { 638 | var code = charToTest.charCodeAt(); 639 | return (code >= 'A'.charCodeAt() && code <= 'Z'.charCodeAt()) || 640 | (code >= 'a'.charCodeAt() && code <= 'z'.charCodeAt()) || 641 | (code >= '0'.charCodeAt() && code <= '9'.charCodeAt()); 642 | } 643 | return false; 644 | }; 645 | 646 | function doComplete(e) { 647 | if(typeof config.completeHandle == 'function') { 648 | var completions = config.completeHandle(promptText, e.shiftKey); 649 | if (!completions) { 650 | return; 651 | } 652 | var len = completions.length; 653 | if (len === 1) { 654 | extern.promptText(completions[0]); 655 | } else if (len > 1 && config.cols) { 656 | var prompt = promptText; 657 | // Compute the number of rows that will fit in the width 658 | var max = 0; 659 | for (var i = 0;i < len;i++) { 660 | max = Math.max(max, completions[i].length); 661 | } 662 | max += 2; 663 | var n = Math.floor(config.cols / max); 664 | var buffer = ""; 665 | var col = 0; 666 | for (i = 0;i < len;i++) { 667 | var completion = completions[i]; 668 | buffer += completions[i]; 669 | for (var j = completion.length;j < max;j++) { 670 | buffer += " "; 671 | } 672 | if (++col >= n) { 673 | buffer += "\n"; 674 | col = 0; 675 | } 676 | } 677 | commandResult(buffer,"jquery-console-message-value"); 678 | extern.promptText(prompt); 679 | } 680 | } 681 | }; 682 | 683 | function doNothing() {}; 684 | 685 | extern.promptText = function(text){ 686 | if (typeof text === 'string') { 687 | promptText = text; 688 | column = promptText.length; 689 | updatePromptDisplay(); 690 | } 691 | return promptText; 692 | }; 693 | 694 | //////////////////////////////////////////////////////////////////////// 695 | // Update the prompt display 696 | function updatePromptDisplay(){ 697 | var line = promptText; 698 | var html = ''; 699 | if (column > 0 && line == ''){ 700 | // When we have an empty line just display a cursor. 701 | html = cursor; 702 | } else if (column == promptText.length){ 703 | // We're at the end of the line, so we need to display 704 | // the text *and* cursor. 705 | html = htmlEncode(line) + cursor; 706 | } else { 707 | // Grab the current character, if there is one, and 708 | // make it the current cursor. 709 | var before = line.substring(0, column); 710 | var current = line.substring(column,column+1); 711 | if (current){ 712 | current = 713 | '' + 714 | htmlEncode(current) + 715 | ''; 716 | } 717 | var after = line.substring(column+1); 718 | html = htmlEncode(before) + current + htmlEncode(after); 719 | } 720 | prompt.html(html); 721 | scrollToBottom(); 722 | }; 723 | 724 | // Simple HTML encoding 725 | // Simply replace '<', '>' and '&' 726 | // TODO: Use jQuery's .html() trick, or grab a proper, fast 727 | // HTML encoder. 728 | function htmlEncode(text){ 729 | return ( 730 | text.replace(/&/g,'&') 731 | .replace(/'); 12 | var curReportFun; 13 | var height = parseInt(window.localStorage.debugconsole_height); 14 | height = !!height ? height : 500; 15 | var heightOffset = height / 10; 16 | var controller = webtermConsole.console({ 17 | continuedPrompt: true, 18 | promptLabel: function () { 19 | return getJSONValue(lastLine); 20 | }, 21 | commandValidate: function () { 22 | return true; 23 | }, 24 | commandHandle: function (line, reportFn) { 25 | curReportFun = reportFn; 26 | if (line.trim().toUpperCase() === "CLS") { 27 | controller.reset(); 28 | $(".jquery-console-inner").append($(".jquery-console-prompt-box").css("display", "inline-block")); 29 | controller.message("", "jquery-console-message-value"); 30 | } else { 31 | lastUserInput = line + "\n"; 32 | if (lastLine.output) { 33 | lastLine.output += lastUserInput; 34 | } else if (lastLine.error) { 35 | lastLine.error += lastUserInput; 36 | } else { 37 | lastLine.output = lastUserInput; 38 | } 39 | _sendCommand(lastUserInput); 40 | controller.resetHistory(); 41 | DisplayAndUpdate(lastLine); 42 | lastLine = { 43 | output: "", 44 | error: "" 45 | }; 46 | DisplayAndUpdate(lastLine); 47 | } 48 | }, 49 | cancelHandle: function () { 50 | //sending CTRL+C character (^C) to the server to cancel the current command 51 | _sendCommand("\x03"); 52 | }, 53 | completeHandle: function (line, reverse) { 54 | return ""; 55 | }, 56 | userInputHandle: function (keycode) { 57 | //reset the string we match on if the user type anything other than tab == 9 58 | if (keycode !== 9 && keycode != 16) { 59 | originalMatchString = undefined; 60 | } 61 | }, 62 | cols: 3, 63 | autofocus: true, 64 | animateScroll: true, 65 | promptHistory: true, 66 | welcomeMessage: "Web Terminal\r\nType 'cls' to clear the console\r\n\r\n" 67 | }); 68 | window.$webtermConsole = $('#webtermConsole'); 69 | window.$webtermConsole.append(webtermConsole); 70 | 71 | var connection = new WebSocketManager.Connection("ws://localhost:5000/cmd"); 72 | window.$webtermConsole.data('connection', connection); 73 | 74 | connection.connectionMethods.onConnected = () => { 75 | console.log("You are now connected! Connection ID: " + connection.connectionId); 76 | }; 77 | 78 | connection.connectionMethods.onDisconnected = () => { 79 | console.log("Disconnected!"); 80 | } 81 | 82 | connection.clientMethods["receiveMessage"] = (message) => { 83 | console.log("receiveMessage: " + message); 84 | DisplayAndUpdate(message); 85 | controller.enableInput(); 86 | }; 87 | 88 | connection.start(); 89 | 90 | function _sendCommand(input) { 91 | console.log("_sendCommand: " + input); 92 | connection.invoke("ReceiveMessage", connection.connectionId, input); 93 | } 94 | 95 | function endsWith(str, suffix) { 96 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 97 | } 98 | 99 | function startsWith(str, prefix) { 100 | return str.indexOf(prefix) == 0; 101 | } 102 | 103 | function getJSONValue(input) { 104 | return input ? (input.output || input.error || "").toString() : ""; 105 | } 106 | 107 | function DisplayAndUpdate(data) { 108 | var prompt = getJSONValue(data); 109 | var lastLinestr = getJSONValue(lastLine); 110 | //this means the last command should be cleared and the next one will be written over it. 111 | // case 1. lastLine = "progress 10%", prompt = "\r" ==> lastLine is not written into HTML (curl) 112 | // case 2. lastLine = "progress 10%\r", prompt = "progress 20%\r" ==> lastLine is not written into HTML (youtube-dl) 113 | // lastLine = "version 123\r", prompt = "\r\n" ==> lastLine IS WRITTEN into HTML (dotnet tsc) 114 | if ((endsWith(prompt, "\r") && !endsWith(lastLinestr, "\n")) || 115 | (endsWith(lastLinestr, "\r") && prompt !== "\r\n" && prompt !== "\n")) { 116 | lastLinestr = ""; 117 | lastLine = null; 118 | } 119 | 120 | var consoleMessages = $(".jquery-console-message"); 121 | if (consoleMessages.length > height && consoleMessages.length % heightOffset == (heightOffset - 1)) { 122 | consoleMessages.slice(0, consoleMessages.length - height).remove(); 123 | } 124 | 125 | //if the data has the same class as the last ".jquery-console-message" 126 | //then just append it to the last one, if not, create a new div. 127 | var lastConsoleMessage = consoleMessages.last(); 128 | lastConsoleMessage.text(lastConsoleMessage.text() + lastLinestr); 129 | lastLine = null; 130 | 131 | //if the prompt is just \r this means that we don't really need to display anything, just marking the line as 132 | if (prompt == "\r") { 133 | return; 134 | } 135 | 136 | // display output, but not updating the HTML 137 | $(".jquery-console-inner").append($(".jquery-console-prompt-box").last().css("display", "inline")); 138 | if (data.error) { 139 | $(".jquery-console-prompt-label").last().text(prompt).css("color", "red"); 140 | } else { 141 | $(".jquery-console-prompt-label").last().text(prompt).css("color", "white"); 142 | } 143 | 144 | controller.promptText(""); 145 | 146 | //Now create the div for the new line that will be printed the next time with the correct class 147 | if (data.error) { 148 | if (!lastConsoleMessage.hasClass("jquery-console-message-error")) { 149 | controller.message("", "jquery-console-message-error"); 150 | } 151 | } else if (!lastConsoleMessage.hasClass("jquery-console-message-value") || endsWith(lastLinestr, "\n")) { 152 | controller.message("", "jquery-console-message-value"); 153 | } 154 | 155 | //save last line for next time. 156 | lastLine = data; 157 | prompt = prompt.trim(); 158 | } 159 | 160 | window.setInterval(function () { 161 | controller.enableInput(); 162 | }, 2000); 163 | } 164 | 165 | $(function () { 166 | LoadConsole(); 167 | }) 168 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/WebSocketManager.Client.TS.njsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 14.0 4 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 5 | WebSocketManager.Client.TS 6 | WebSocketManager.Client.TS 7 | 8 | 9 | 10 | Debug 11 | 2.0 12 | d62c48e8-ec40-4904-8e59-f33ef4dcde25 13 | . 14 | server.ts 15 | True 16 | 17 | 18 | . 19 | . 20 | v4.0 21 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 22 | 1337 23 | true 24 | CommonJS 25 | true 26 | true 27 | 28 | 29 | true 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | False 47 | True 48 | 0 49 | / 50 | http://localhost:48022/ 51 | False 52 | True 53 | http://localhost:1337 54 | False 55 | 56 | 57 | 58 | 59 | 60 | 61 | CurrentPage 62 | True 63 | False 64 | False 65 | False 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | False 75 | False 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/dist/WebSocketManager.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define("WebSocketManager", [], factory); 6 | else if(typeof exports === 'object') 7 | exports["WebSocketManager"] = factory(); 8 | else 9 | root["WebSocketManager"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | /******/ 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | module.exports = __webpack_require__(1); 58 | 59 | 60 | /***/ }, 61 | /* 1 */ 62 | /***/ function(module, exports, __webpack_require__) { 63 | 64 | "use strict"; 65 | var InvocationDescriptor_1 = __webpack_require__(2); 66 | var Message_1 = __webpack_require__(3); 67 | var Connection = (function () { 68 | function Connection(url, enableLogging) { 69 | var _this = this; 70 | if (enableLogging === void 0) { enableLogging = false; } 71 | this.enableLogging = false; 72 | this.clientMethods = {}; 73 | this.connectionMethods = {}; 74 | this.url = url; 75 | this.enableLogging = enableLogging; 76 | this.connectionMethods['onConnected'] = function () { 77 | if (_this.enableLogging) { 78 | console.log('Connected! connectionId: ' + _this.connectionId); 79 | } 80 | }; 81 | this.connectionMethods['onDisconnected'] = function () { 82 | if (_this.enableLogging) { 83 | console.log('Connection closed from: ' + _this.url); 84 | } 85 | }; 86 | this.connectionMethods['onOpen'] = function (socketOpenedEvent) { 87 | if (_this.enableLogging) { 88 | console.log('WebSockets connection opened!'); 89 | } 90 | }; 91 | } 92 | Connection.prototype.start = function () { 93 | var _this = this; 94 | this.socket = new WebSocket(this.url); 95 | this.socket.onopen = function (event) { 96 | _this.connectionMethods['onOpen'].apply(_this, event); 97 | }; 98 | this.socket.onmessage = function (event) { 99 | _this.message = JSON.parse(event.data); 100 | if (_this.message.messageType == Message_1.MessageType.Text) { 101 | if (_this.enableLogging) { 102 | console.log('Text message received. Message: ' + _this.message.data); 103 | } 104 | } 105 | else if (_this.message.messageType == Message_1.MessageType.MethodInvocation) { 106 | var invocationDescriptor = JSON.parse(_this.message.data); 107 | _this.clientMethods[invocationDescriptor.methodName].apply(_this, invocationDescriptor.arguments); 108 | } 109 | else if (_this.message.messageType == Message_1.MessageType.ConnectionEvent) { 110 | _this.connectionId = _this.message.data; 111 | _this.connectionMethods['onConnected'].apply(_this); 112 | } 113 | }; 114 | this.socket.onclose = function (event) { 115 | _this.connectionMethods['onDisconnected'].apply(_this); 116 | }; 117 | this.socket.onerror = function (event) { 118 | if (_this.enableLogging) { 119 | console.log('Error data: ' + event.error); 120 | } 121 | }; 122 | }; 123 | Connection.prototype.invoke = function (methodName) { 124 | var args = []; 125 | for (var _i = 1; _i < arguments.length; _i++) { 126 | args[_i - 1] = arguments[_i]; 127 | } 128 | var invocationDescriptor = new InvocationDescriptor_1.InvocationDescriptor(methodName, args); 129 | if (this.enableLogging) { 130 | console.log(invocationDescriptor); 131 | } 132 | this.socket.send(JSON.stringify(invocationDescriptor)); 133 | }; 134 | return Connection; 135 | }()); 136 | exports.Connection = Connection; 137 | 138 | 139 | /***/ }, 140 | /* 2 */ 141 | /***/ function(module, exports) { 142 | 143 | "use strict"; 144 | var InvocationDescriptor = (function () { 145 | function InvocationDescriptor(methodName, args) { 146 | this.methodName = methodName; 147 | this.arguments = args; 148 | } 149 | return InvocationDescriptor; 150 | }()); 151 | exports.InvocationDescriptor = InvocationDescriptor; 152 | 153 | 154 | /***/ }, 155 | /* 3 */ 156 | /***/ function(module, exports) { 157 | 158 | "use strict"; 159 | (function (MessageType) { 160 | MessageType[MessageType["Text"] = 0] = "Text"; 161 | MessageType[MessageType["MethodInvocation"] = 1] = "MethodInvocation"; 162 | MessageType[MessageType["ConnectionEvent"] = 2] = "ConnectionEvent"; 163 | })(exports.MessageType || (exports.MessageType = {})); 164 | var MessageType = exports.MessageType; 165 | var Message = (function () { 166 | function Message() { 167 | } 168 | return Message; 169 | }()); 170 | exports.Message = Message; 171 | 172 | 173 | /***/ } 174 | /******/ ]) 175 | }); 176 | ; 177 | //# sourceMappingURL=WebSocketManager.js.map -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-manager-typescript-client", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "dist/WebSocketManager.min.js", 6 | "scripts": { 7 | "prepublish": "webpack --debug; webpack -p" 8 | }, 9 | "author": "Radu Matei ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "mocha": "^2.4.5", 13 | "ts-loader": "^0.8.1", 14 | "tslint": "^3.5.0", 15 | "tslint-loader": "^2.1.0", 16 | "typescript": "^1.8.2", 17 | "webpack": "^1.12.14", 18 | "yargs": "^4.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/src/Connection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var InvocationDescriptor_1 = require("./InvocationDescriptor"); 3 | var Message_1 = require("./Message"); 4 | var Connection = (function () { 5 | function Connection(url, enableLogging) { 6 | if (enableLogging === void 0) { enableLogging = false; } 7 | var _this = this; 8 | this.enableLogging = false; 9 | this.clientMethods = {}; 10 | this.connectionMethods = {}; 11 | this.url = url; 12 | this.enableLogging = enableLogging; 13 | this.connectionMethods['onConnected'] = function () { 14 | if (_this.enableLogging) { 15 | console.log('Connected! connectionId: ' + _this.connectionId); 16 | } 17 | }; 18 | this.connectionMethods['onDisconnected'] = function () { 19 | if (_this.enableLogging) { 20 | console.log('Connection closed from: ' + _this.url); 21 | } 22 | }; 23 | this.connectionMethods['onOpen'] = function (socketOpenedEvent) { 24 | if (_this.enableLogging) { 25 | console.log('WebSockets connection opened!'); 26 | } 27 | }; 28 | } 29 | Connection.prototype.start = function () { 30 | var _this = this; 31 | this.socket = new WebSocket(this.url); 32 | this.socket.onopen = function (event) { 33 | _this.connectionMethods['onOpen'].apply(_this, event); 34 | }; 35 | this.socket.onmessage = function (event) { 36 | _this.message = JSON.parse(event.data); 37 | if (_this.message.messageType == Message_1.MessageType.Text) { 38 | if (_this.enableLogging) { 39 | console.log('Text message received. Message: ' + _this.message.data); 40 | } 41 | } 42 | else if (_this.message.messageType == Message_1.MessageType.MethodInvocation) { 43 | var invocationDescriptor = JSON.parse(_this.message.data); 44 | _this.clientMethods[invocationDescriptor.methodName].apply(_this, invocationDescriptor.arguments); 45 | } 46 | else if (_this.message.messageType == Message_1.MessageType.ConnectionEvent) { 47 | _this.connectionId = _this.message.data; 48 | _this.connectionMethods['onConnected'].apply(_this); 49 | } 50 | }; 51 | this.socket.onclose = function (event) { 52 | _this.connectionMethods['onDisconnected'].apply(_this); 53 | }; 54 | this.socket.onerror = function (event) { 55 | if (_this.enableLogging) { 56 | console.log('Error data: ' + event.error); 57 | } 58 | }; 59 | }; 60 | Connection.prototype.invoke = function (methodName) { 61 | var args = []; 62 | for (var _i = 1; _i < arguments.length; _i++) { 63 | args[_i - 1] = arguments[_i]; 64 | } 65 | var invocationDescriptor = new InvocationDescriptor_1.InvocationDescriptor(methodName, args); 66 | if (this.enableLogging) { 67 | console.log(invocationDescriptor); 68 | } 69 | this.socket.send(JSON.stringify(invocationDescriptor)); 70 | }; 71 | return Connection; 72 | }()); 73 | exports.Connection = Connection; 74 | //# sourceMappingURL=Connection.js.map -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/src/Connection.ts: -------------------------------------------------------------------------------- 1 | import { InvocationDescriptor } from './InvocationDescriptor' 2 | import { Message, MessageType } from './Message' 3 | 4 | export class Connection { 5 | 6 | public url: string; 7 | public connectionId: string; 8 | public enableLogging: boolean = false; 9 | 10 | protected message: Message; 11 | protected socket: WebSocket; 12 | 13 | public clientMethods: { [s: string]: Function; } = {}; 14 | public connectionMethods: { [s: string]: Function; } = {}; 15 | 16 | constructor(url: string, enableLogging: boolean=false) { 17 | this.url = url; 18 | 19 | this.enableLogging = enableLogging; 20 | 21 | this.connectionMethods['onConnected'] = () => { 22 | if(this.enableLogging) { 23 | console.log('Connected! connectionId: ' + this.connectionId); 24 | } 25 | } 26 | 27 | this.connectionMethods['onDisconnected'] = () => { 28 | if(this.enableLogging) { 29 | console.log('Connection closed from: ' + this.url); 30 | } 31 | } 32 | 33 | this.connectionMethods['onOpen'] = (socketOpenedEvent: any) => { 34 | if(this.enableLogging) { 35 | console.log('WebSockets connection opened!'); 36 | } 37 | } 38 | } 39 | 40 | public start() { 41 | this.socket = new WebSocket(this.url); 42 | 43 | this.socket.onopen = (event: MessageEvent) => { 44 | this.connectionMethods['onOpen'].apply(this, event); 45 | }; 46 | 47 | this.socket.onmessage = (event: MessageEvent) => { 48 | this.message = JSON.parse(event.data); 49 | 50 | if (this.message.messageType == MessageType.Text) { 51 | if(this.enableLogging) { 52 | console.log('Text message received. Message: ' + this.message.data); 53 | } 54 | } 55 | 56 | else if (this.message.messageType == MessageType.MethodInvocation) { 57 | let invocationDescriptor: InvocationDescriptor = JSON.parse(this.message.data); 58 | 59 | this.clientMethods[invocationDescriptor.methodName].apply(this, invocationDescriptor.arguments); 60 | } 61 | 62 | else if (this.message.messageType == MessageType.ConnectionEvent) { 63 | this.connectionId = this.message.data; 64 | this.connectionMethods['onConnected'].apply(this); 65 | } 66 | } 67 | 68 | this.socket.onclose = (event: CloseEvent) => { 69 | this.connectionMethods['onDisconnected'].apply(this); 70 | } 71 | 72 | this.socket.onerror = (event: ErrorEvent) => { 73 | if(this.enableLogging) { 74 | console.log('Error data: ' + event.error); 75 | } 76 | } 77 | } 78 | 79 | public invoke(methodName: string, ...args: any[]) { 80 | let invocationDescriptor = new InvocationDescriptor(methodName, args); 81 | 82 | if(this.enableLogging) { 83 | console.log(invocationDescriptor); 84 | } 85 | 86 | this.socket.send(JSON.stringify(invocationDescriptor)); 87 | } 88 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/src/InvocationDescriptor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var InvocationDescriptor = (function () { 3 | function InvocationDescriptor(methodName, args) { 4 | this.methodName = methodName; 5 | this.arguments = args; 6 | } 7 | return InvocationDescriptor; 8 | }()); 9 | exports.InvocationDescriptor = InvocationDescriptor; 10 | //# sourceMappingURL=InvocationDescriptor.js.map -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/src/InvocationDescriptor.ts: -------------------------------------------------------------------------------- 1 | export class InvocationDescriptor { 2 | public methodName: string; 3 | public arguments: Array; 4 | 5 | constructor(methodName: string, args: any[]) { 6 | this.methodName = methodName; 7 | this.arguments = args; 8 | } 9 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/src/Message.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var MessageType; 3 | (function (MessageType) { 4 | MessageType[MessageType["Text"] = 0] = "Text"; 5 | MessageType[MessageType["MethodInvocation"] = 1] = "MethodInvocation"; 6 | MessageType[MessageType["ConnectionEvent"] = 2] = "ConnectionEvent"; 7 | })(MessageType = exports.MessageType || (exports.MessageType = {})); 8 | var Message = (function () { 9 | function Message() { 10 | } 11 | return Message; 12 | }()); 13 | exports.Message = Message; 14 | //# sourceMappingURL=Message.js.map -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/src/Message.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | Text = 0, 3 | MethodInvocation = 1, 4 | ConnectionEvent = 2 5 | } 6 | 7 | export class Message { 8 | public messageType: MessageType; 9 | public data: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "react", 6 | "module": "commonjs", 7 | "noImplicitAny": true, 8 | // "preserveConstEnums": true, 9 | "removeComments": false, 10 | "sourceMap": true, 11 | "target": "es5" 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "quotemark": [ true, "single" ] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client.TS/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'), 2 | path = require('path'), 3 | yargs = require('yargs'); 4 | 5 | var libraryName = 'WebSocketManager', 6 | plugins = [], 7 | outputFile; 8 | 9 | if (yargs.argv.p) { 10 | plugins.push(new webpack.optimize.UglifyJsPlugin({ minimize: true })); 11 | outputFile = libraryName + '.min.js'; 12 | } else { 13 | outputFile = libraryName + '.js'; 14 | } 15 | 16 | var config = { 17 | entry: [ 18 | __dirname + '/src/Connection.ts' 19 | ], 20 | devtool: 'source-map', 21 | output: { 22 | path: path.join(__dirname, '/dist'), 23 | filename: outputFile, 24 | library: libraryName, 25 | libraryTarget: 'umd', 26 | umdNamedDefine: true 27 | }, 28 | module: { 29 | preLoaders: [ 30 | { test: /\.tsx?$/, loader: 'tslint', exclude: /node_modules/ } 31 | ], 32 | loaders: [ 33 | { test: /\.tsx?$/, loader: 'ts', exclude: /node_modules/ } 34 | ] 35 | }, 36 | resolve: { 37 | root: path.resolve('./src'), 38 | extensions: [ '', '.js', '.ts', '.jsx', '.tsx' ] 39 | }, 40 | plugins: plugins, 41 | 42 | // Individual Plugin Options 43 | tslint: { 44 | emitErrors: true, 45 | failOnHint: true 46 | } 47 | }; 48 | 49 | module.exports = config; 50 | -------------------------------------------------------------------------------- /src/WebSocketManager.Client/Connection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Serialization; 10 | 11 | using WebSocketManager.Common; 12 | 13 | namespace WebSocketManager.Client 14 | { 15 | public class Connection 16 | { 17 | public string ConnectionId { get; set; } 18 | 19 | private ClientWebSocket _clientWebSocket { get; set; } 20 | 21 | private JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings() 22 | { 23 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 24 | TypeNameHandling = TypeNameHandling.All, 25 | TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, 26 | SerializationBinder = new JsonBinderWithoutAssembly() 27 | }; 28 | 29 | /// 30 | /// Gets the method invocation strategy. 31 | /// 32 | /// The method invocation strategy. 33 | public MethodInvocationStrategy MethodInvocationStrategy { get; } 34 | 35 | /// 36 | /// The waiting remote invocations for Client to Server method calls. 37 | /// 38 | private Dictionary> _waitingRemoteInvocations = new Dictionary>(); 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | /// The method invocation strategy used for incoming requests. 44 | public Connection(MethodInvocationStrategy methodInvocationStrategy) 45 | { 46 | MethodInvocationStrategy = methodInvocationStrategy; 47 | _jsonSerializerSettings.Converters.Insert(0, new PrimitiveJsonConverter()); 48 | } 49 | 50 | public async Task StartConnectionAsync(string uri) 51 | { 52 | // also check if connection was lost, that's probably why we get called multiple times. 53 | if (_clientWebSocket == null || _clientWebSocket.State != WebSocketState.Open) 54 | { 55 | // create a new web-socket so the next connect call works. 56 | _clientWebSocket?.Dispose(); 57 | _clientWebSocket = new ClientWebSocket(); 58 | } 59 | // don't do anything, we are already connected. 60 | else return; 61 | 62 | await _clientWebSocket.ConnectAsync(new Uri(uri), CancellationToken.None).ConfigureAwait(false); 63 | 64 | await Receive(_clientWebSocket, async (receivedMessage) => 65 | { 66 | if (receivedMessage.MessageType == MessageType.ConnectionEvent) 67 | { 68 | this.ConnectionId = receivedMessage.Data; 69 | } 70 | else if (receivedMessage.MessageType == MessageType.MethodInvocation) 71 | { 72 | // retrieve the method invocation request. 73 | InvocationDescriptor invocationDescriptor = null; 74 | try 75 | { 76 | invocationDescriptor = JsonConvert.DeserializeObject(receivedMessage.Data, _jsonSerializerSettings); 77 | if (invocationDescriptor == null) return; 78 | } 79 | catch { return; } // ignore invalid data sent to the client. 80 | 81 | // if the unique identifier hasn't been set then the server doesn't want a return value. 82 | if (invocationDescriptor.Identifier == Guid.Empty) 83 | { 84 | // invoke the method only. 85 | try 86 | { 87 | await MethodInvocationStrategy.OnInvokeMethodReceivedAsync(_clientWebSocket, invocationDescriptor); 88 | } 89 | catch (Exception) 90 | { 91 | // we consume all exceptions. 92 | } 93 | } 94 | else 95 | { 96 | // invoke the method and get the result. 97 | InvocationResult invokeResult; 98 | try 99 | { 100 | // create an invocation result with the results. 101 | invokeResult = new InvocationResult() 102 | { 103 | Identifier = invocationDescriptor.Identifier, 104 | Result = await MethodInvocationStrategy.OnInvokeMethodReceivedAsync(_clientWebSocket, invocationDescriptor), 105 | Exception = null 106 | }; 107 | } 108 | // send the exception as the invocation result if there was one. 109 | catch (Exception ex) 110 | { 111 | invokeResult = new InvocationResult() 112 | { 113 | Identifier = invocationDescriptor.Identifier, 114 | Result = null, 115 | Exception = new RemoteException(ex) 116 | }; 117 | } 118 | 119 | // send a message to the server containing the result. 120 | var message = new Message() 121 | { 122 | MessageType = MessageType.MethodReturnValue, 123 | Data = JsonConvert.SerializeObject(invokeResult, _jsonSerializerSettings) 124 | }; 125 | var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message, _jsonSerializerSettings)); 126 | await _clientWebSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); 127 | } 128 | } 129 | else if (receivedMessage.MessageType == MessageType.MethodReturnValue) 130 | { 131 | var invocationResult = JsonConvert.DeserializeObject(receivedMessage.Data, _jsonSerializerSettings); 132 | // find the completion source in the waiting list. 133 | if (_waitingRemoteInvocations.ContainsKey(invocationResult.Identifier)) 134 | { 135 | // set the result of the completion source so the invoke method continues executing. 136 | _waitingRemoteInvocations[invocationResult.Identifier].SetResult(invocationResult); 137 | // remove the completion source from the waiting list. 138 | _waitingRemoteInvocations.Remove(invocationResult.Identifier); 139 | } 140 | } 141 | }); 142 | } 143 | 144 | /// 145 | /// Send a method invoke request to the server and waits for a reply. 146 | /// 147 | /// Example usage: set the MethodName to SendMessage and set the arguments to the connectionID with a text message 148 | /// An awaitable task with the return value on success. 149 | public async Task SendAsync(InvocationDescriptor invocationDescriptor) 150 | { 151 | // generate a unique identifier for this invocation. 152 | invocationDescriptor.Identifier = Guid.NewGuid(); 153 | 154 | // add ourselves to the waiting list for return values. 155 | TaskCompletionSource task = new TaskCompletionSource(); 156 | // after a timeout of 60 seconds we will cancel the task and remove it from the waiting list. 157 | new CancellationTokenSource(1000 * 60).Token.Register(() => { _waitingRemoteInvocations.Remove(invocationDescriptor.Identifier); task.TrySetCanceled(); }); 158 | _waitingRemoteInvocations.Add(invocationDescriptor.Identifier, task); 159 | 160 | // send the method invocation to the server. 161 | var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new Message { MessageType = MessageType.MethodInvocation, Data = JsonConvert.SerializeObject(invocationDescriptor, _jsonSerializerSettings) }, _jsonSerializerSettings)); 162 | await _clientWebSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None); 163 | 164 | // wait for the return value elsewhere in the program. 165 | InvocationResult result = await task.Task; 166 | 167 | // ... we just got an answer. 168 | 169 | // if we have completed successfully: 170 | if (task.Task.IsCompleted) 171 | { 172 | // there was a remote exception so we throw it here. 173 | if (result.Exception != null) 174 | throw new Exception(result.Exception.Message); 175 | 176 | // return the value. 177 | 178 | // support null. 179 | if (result.Result == null) return default(T); 180 | // cast anything to T and hope it works. 181 | return (T)result.Result; 182 | } 183 | 184 | // if we reach here we got cancelled or alike so throw a timeout exception. 185 | throw new TimeoutException(); // todo: insert fancy message here. 186 | } 187 | 188 | /// 189 | /// Send a method invoke request to the server. 190 | /// 191 | /// Example usage: set the MethodName to SendMessage and set the arguments to the connectionID with a text message 192 | /// An awaitable task. 193 | public async Task SendAsync(InvocationDescriptor invocationDescriptor) 194 | { 195 | // send the method invocation to the server. 196 | var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new Message { MessageType = MessageType.MethodInvocation, Data = JsonConvert.SerializeObject(invocationDescriptor) })); 197 | await _clientWebSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None); 198 | } 199 | 200 | public async Task StopConnectionAsync() 201 | { 202 | await _clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); 203 | } 204 | 205 | private async Task Receive(ClientWebSocket clientWebSocket, Action handleMessage) 206 | { 207 | while (_clientWebSocket.State == WebSocketState.Open) 208 | { 209 | ArraySegment buffer = new ArraySegment(new Byte[1024 * 4]); 210 | string serializedMessage = null; 211 | WebSocketReceiveResult result = null; 212 | using (var ms = new MemoryStream()) 213 | { 214 | do 215 | { 216 | result = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); 217 | ms.Write(buffer.Array, buffer.Offset, result.Count); 218 | } 219 | while (!result.EndOfMessage); 220 | 221 | ms.Seek(0, SeekOrigin.Begin); 222 | 223 | using (var reader = new StreamReader(ms, Encoding.UTF8)) 224 | { 225 | serializedMessage = await reader.ReadToEndAsync().ConfigureAwait(false); 226 | } 227 | } 228 | 229 | if (result.MessageType == WebSocketMessageType.Text) 230 | { 231 | var message = JsonConvert.DeserializeObject(serializedMessage, _jsonSerializerSettings); 232 | handleMessage(message); 233 | } 234 | else if (result.MessageType == WebSocketMessageType.Close) 235 | { 236 | await _clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait(false); 237 | break; 238 | } 239 | } 240 | } 241 | 242 | public async Task SendOnlyAsync(string method) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { } }); 243 | 244 | public async Task SendOnlyAsync(string method, T1 arg1) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1 } }); 245 | 246 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2 } }); 247 | 248 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3 } }); 249 | 250 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4 } }); 251 | 252 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5 } }); 253 | 254 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6 } }); 255 | 256 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7 } }); 257 | 258 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 } }); 259 | 260 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 } }); 261 | 262 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 } }); 263 | 264 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 } }); 265 | 266 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12 } }); 267 | 268 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13 } }); 269 | 270 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14 } }); 271 | 272 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15 } }); 273 | 274 | public async Task SendOnlyAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16 } }); 275 | 276 | public async Task SendAsync(string method) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { } }); 277 | 278 | public async Task SendAsync(string method, T1 arg1) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1 } }); 279 | 280 | public async Task SendAsync(string method, T1 arg1, T2 arg2) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2 } }); 281 | 282 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3 } }); 283 | 284 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4 } }); 285 | 286 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5 } }); 287 | 288 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6 } }); 289 | 290 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7 } }); 291 | 292 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 } }); 293 | 294 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 } }); 295 | 296 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 } }); 297 | 298 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 } }); 299 | 300 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12 } }); 301 | 302 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13 } }); 303 | 304 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14 } }); 305 | 306 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15 } }); 307 | 308 | public async Task SendAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16) => await SendAsync(new InvocationDescriptor { MethodName = method, Arguments = new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16 } }); 309 | } 310 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Client/WebSocketManager.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 0.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Json/JsonBinderWithoutAssembly.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Serialization; 2 | using System; 3 | 4 | namespace WebSocketManager.Common 5 | { 6 | /// 7 | /// Finds types without looking at the assembly. 8 | /// 9 | /// 10 | public class JsonBinderWithoutAssembly : ISerializationBinder 11 | { 12 | public void BindToName(Type serializedType, out string assemblyName, out string typeName) 13 | { 14 | typeName = serializedType.FullName; 15 | assemblyName = null; 16 | } 17 | 18 | public Type BindToType(string assemblyName, string typeName) 19 | { 20 | return Type.GetType(typeName); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Json/PrimitiveJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Runtime.Serialization.Formatters; 4 | 5 | namespace WebSocketManager.Common 6 | { 7 | /// 8 | /// https://stackoverflow.com/questions/25007001/json-net-does-not-preserve-primitive-type-information-in-lists-or-dictionaries-o?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa 9 | /// Shoutouts to Sacrilege for his awesome primitive json converter. 10 | /// 11 | /// 12 | public sealed class PrimitiveJsonConverter : JsonConverter 13 | { 14 | public PrimitiveJsonConverter() 15 | { 16 | } 17 | 18 | public override bool CanRead 19 | { 20 | get 21 | { 22 | return false; 23 | } 24 | } 25 | 26 | public override bool CanConvert(Type objectType) 27 | { 28 | return objectType.IsPrimitive || objectType == typeof(Guid) || objectType == typeof(string); 29 | } 30 | 31 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 37 | { 38 | switch (serializer.TypeNameHandling) 39 | { 40 | case TypeNameHandling.All: 41 | writer.WriteStartObject(); 42 | writer.WritePropertyName("$type", false); 43 | 44 | switch (serializer.TypeNameAssemblyFormatHandling) 45 | { 46 | case TypeNameAssemblyFormatHandling.Full: 47 | writer.WriteValue(value.GetType().AssemblyQualifiedName); 48 | break; 49 | 50 | default: 51 | writer.WriteValue(value.GetType().FullName); 52 | break; 53 | } 54 | 55 | writer.WritePropertyName("$value", false); 56 | writer.WriteValue(value); 57 | writer.WriteEndObject(); 58 | break; 59 | 60 | default: 61 | writer.WriteValue(value); 62 | break; 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Networking/InvocationDescriptor.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace WebSocketManager.Common 5 | { 6 | /// 7 | /// Represents a method name with parameters that is to be executed remotely. 8 | /// 9 | public class InvocationDescriptor 10 | { 11 | /// 12 | /// Gets or sets the name of the remote method. 13 | /// 14 | /// The name of the remote method. 15 | [JsonProperty("methodName")] 16 | public string MethodName { get; set; } 17 | 18 | /// 19 | /// Gets or sets the arguments passed to the method. 20 | /// 21 | /// The arguments passed to the method. 22 | [JsonProperty("arguments")] 23 | public object[] Arguments { get; set; } 24 | 25 | /// 26 | /// Gets or sets the unique identifier used to associate return values with this call. 27 | /// 28 | /// The unique identifier of the invocation. 29 | [JsonProperty("identifier")] 30 | public Guid Identifier { get; set; } = Guid.Empty; 31 | } 32 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Networking/InvocationResult.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace WebSocketManager.Common 5 | { 6 | /// 7 | /// Represents the return value of a method that was executed remotely. 8 | /// 9 | public class InvocationResult 10 | { 11 | /// 12 | /// Gets or sets the unique identifier associated with the invocation. 13 | /// 14 | /// The unique identifier of the invocation. 15 | [JsonProperty("identifier")] 16 | public Guid Identifier { get; set; } 17 | 18 | /// 19 | /// Gets or sets the result of the method call. 20 | /// 21 | /// The result of the method call. 22 | [JsonProperty("result")] 23 | public object Result { get; set; } 24 | 25 | /// 26 | /// Gets or sets the remote exception the method call caused. 27 | /// 28 | /// The remote exception of the method call. 29 | [JsonProperty("exception")] 30 | public RemoteException Exception { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Networking/Message.cs: -------------------------------------------------------------------------------- 1 | namespace WebSocketManager.Common 2 | { 3 | public enum MessageType 4 | { 5 | Text, 6 | MethodInvocation, 7 | ConnectionEvent, 8 | MethodReturnValue 9 | } 10 | 11 | public class Message 12 | { 13 | public MessageType MessageType { get; set; } 14 | public string Data { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Networking/RemoteException.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace WebSocketManager.Common 5 | { 6 | /// 7 | /// An exception that occured remotely. 8 | /// 9 | public class RemoteException 10 | { 11 | /// 12 | /// Gets or sets the exception message. 13 | /// 14 | /// The exception message. 15 | [JsonProperty("message")] 16 | public string Message { get; set; } = $"A remote exception occured"; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public RemoteException() 22 | { 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The exception that occured. 29 | public RemoteException(Exception exception) 30 | { 31 | Message = $"A remote exception occured: '{exception.Message}'."; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Strategies/ControllerMethodInvocationStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebSocketManager.Common 10 | { 11 | /// 12 | /// The controller method invocation strategy. Finds methods in a single class using reflection. 13 | /// 14 | /// 15 | public class ControllerMethodInvocationStrategy : MethodInvocationStrategy 16 | { 17 | /// 18 | /// Gets the controller containing the methods. 19 | /// 20 | /// The controller containing the methods. 21 | public object Controller { get; set; } 22 | 23 | /// 24 | /// Gets the method name prefix. This prevents users from calling methods they aren't 25 | /// supposed to call. You could for example use the awesome 'ᐅ' character. 26 | /// 27 | /// The method name prefix. 28 | public string Prefix { get; } = ""; 29 | 30 | /// 31 | /// Gets a value indicating whether there is no websocket argument (useful for client-side methods). 32 | /// 33 | /// true if there is no websocket argument; otherwise, false. 34 | public bool NoWebsocketArgument { get; set; } = false; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | public ControllerMethodInvocationStrategy() 40 | { 41 | } 42 | 43 | /// 44 | /// Initializes a new instance of the class. 45 | /// 46 | /// The controller containing the methods. 47 | public ControllerMethodInvocationStrategy(object controller) 48 | { 49 | Controller = controller; 50 | } 51 | 52 | /// 53 | /// Initializes a new instance of the class. 54 | /// 55 | /// 56 | /// The method name prefix. This prevents users from calling methods they aren't supposed to 57 | /// call. You could for example use the awesome 'ᐅ' character. 58 | /// 59 | /// The controller containing the methods. 60 | public ControllerMethodInvocationStrategy(string prefix, object controller) 61 | { 62 | Prefix = prefix; 63 | Controller = controller; 64 | } 65 | 66 | /// 67 | /// Called when an invoke method call has been received. 68 | /// 69 | /// The web-socket of the client that wants to invoke a method. 70 | /// 71 | /// The invocation descriptor containing the method name and parameters. 72 | /// 73 | /// Awaitable Task. 74 | public override async Task OnInvokeMethodReceivedAsync(WebSocket socket, InvocationDescriptor invocationDescriptor) 75 | { 76 | // create the method name that has to be found. 77 | string command = Prefix + invocationDescriptor.MethodName; 78 | 79 | // use reflection to find the method in the desired controller. 80 | MethodInfo method = Controller.GetType().GetMethod(command); 81 | // if the method could not be found: 82 | if (method == null) throw new Exception($"Received unknown command '{command}' for controller '{Controller.GetType().Name}'."); 83 | 84 | // optionally insert client as parameter. 85 | List args = invocationDescriptor.Arguments.ToList(); 86 | if (!NoWebsocketArgument) 87 | args.Insert(0, socket); 88 | 89 | // call the method asynchronously. 90 | try 91 | { 92 | return await Task.Run(() => method.Invoke(Controller, args.ToArray())); 93 | } 94 | catch (TargetInvocationException ex) 95 | { 96 | throw ex.InnerException; 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Strategies/DecoratedControllerMethodInvocationStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebSocketManager.Common 10 | { 11 | /// 12 | /// The decorated controller method invocation strategy. Finds methods in several classes using reflection. 13 | /// 14 | /// 15 | public class DecoratedControllerMethodInvocationStrategy : MethodInvocationStrategy 16 | { 17 | /// 18 | /// Gets the method name prefix. This prevents users from calling methods they aren't 19 | /// supposed to call. You could for example use the awesome 'ᐅ' character. 20 | /// 21 | /// The method name prefix. 22 | public string Prefix { get; } = ""; 23 | 24 | /// 25 | /// Gets the prefix and method name separator. Default value is a forward slash '/'. 26 | /// 27 | /// The separator to separate prefix and method name. 28 | public char Separator { get; } = '/'; 29 | 30 | /// 31 | /// Gets a value indicating whether there is no websocket argument (useful for client-side methods). 32 | /// 33 | /// true if there is no websocket argument; otherwise, false. 34 | public bool NoWebsocketArgument { get; set; } = false; 35 | 36 | /// 37 | /// Gets the registered controllers. 38 | /// 39 | /// The registered controllers. 40 | public Dictionary Controllers { get; } = new Dictionary(); 41 | 42 | /// 43 | /// Initializes a new instance of the class. 45 | /// 46 | public DecoratedControllerMethodInvocationStrategy() 47 | { 48 | } 49 | 50 | /// 51 | /// Initializes a new instance of the class. 53 | /// 54 | /// 55 | /// The method name prefix. This prevents users from calling methods they aren't supposed to 56 | /// call. You could for example use the awesome 'ᐅ' character. 57 | /// 58 | public DecoratedControllerMethodInvocationStrategy(string prefix) 59 | { 60 | Prefix = prefix; 61 | } 62 | 63 | /// 64 | /// Initializes a new instance of the class. 66 | /// 67 | /// 68 | /// The method name prefix. This prevents users from calling methods they aren't supposed to 69 | /// call. You could for example use the awesome 'ᐅ' character. 70 | /// 71 | /// The prefix and method name separator. Default value is a forward slash '/'. 72 | public DecoratedControllerMethodInvocationStrategy(string prefix, char separator) 73 | { 74 | Prefix = prefix; 75 | Separator = separator; 76 | } 77 | 78 | /// 79 | /// Registers the specified controller to the specified prefix. 80 | /// 81 | /// The controller prefix (e.g. "session"). 82 | /// The controller containing the methods. 83 | public void Register(string prefix, object controller) 84 | { 85 | Controllers.Add(prefix.ToLower(), controller); 86 | } 87 | 88 | /// 89 | /// Called when an invoke method call has been received. 90 | /// 91 | /// The web-socket of the client that wants to invoke a method. 92 | /// 93 | /// The invocation descriptor containing the method name and parameters. 94 | /// 95 | /// Awaitable Task. 96 | public override async Task OnInvokeMethodReceivedAsync(WebSocket socket, InvocationDescriptor invocationDescriptor) 97 | { 98 | // there must be a separator in the method name. 99 | if (!invocationDescriptor.MethodName.Contains(Separator)) throw new Exception($"Invalid controller or method name '{invocationDescriptor.MethodName}'."); 100 | 101 | // find the controller and the method name. 102 | string[] names = invocationDescriptor.MethodName.Split(Separator); 103 | string controller = names[0].ToLower(); 104 | string command = Prefix + names[1]; 105 | 106 | // find the desired controller. 107 | if (Controllers.TryGetValue(controller, out object self)) 108 | { 109 | // use reflection to find the method in the desired controller. 110 | MethodInfo method = self.GetType().GetMethod(command); 111 | 112 | // if the method could not be found: 113 | if (method == null) 114 | throw new Exception($"Received unknown command '{command}' for controller '{controller}'."); 115 | 116 | // optionally insert client as parameter. 117 | List args = invocationDescriptor.Arguments.ToList(); 118 | if (!NoWebsocketArgument) 119 | args.Insert(0, socket); 120 | 121 | // call the method asynchronously. 122 | try 123 | { 124 | return await Task.Run(() => method.Invoke(self, args.ToArray())); 125 | } 126 | catch (TargetInvocationException ex) 127 | { 128 | throw ex.InnerException; 129 | } 130 | } 131 | else throw new Exception($"Received command '{command}' for unknown controller '{controller}'."); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Strategies/MethodInvocationStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace WebSocketManager.Common 9 | { 10 | /// 11 | /// The base class of all method invocation strategies. 12 | /// 13 | public abstract class MethodInvocationStrategy 14 | { 15 | /// 16 | /// Called when an invoke method call has been received. 17 | /// 18 | /// The web-socket of the client that wants to invoke a method. 19 | /// 20 | /// The invocation descriptor containing the method name and parameters. 21 | /// 22 | /// Awaitable Task. 23 | public virtual Task OnInvokeMethodReceivedAsync(WebSocket socket, InvocationDescriptor invocationDescriptor) 24 | { 25 | throw new NotImplementedException(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/Strategies/StringMethodInvocationStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace WebSocketManager.Common 9 | { 10 | /// 11 | /// The string method invocation strategy. Finds methods by registering the names and callbacks. 12 | /// 13 | public class StringMethodInvocationStrategy : MethodInvocationStrategy 14 | { 15 | /// 16 | /// The registered handlers. 17 | /// 18 | private Dictionary _handlers = new Dictionary(); 19 | 20 | /// 21 | /// Registers the specified method name and calls the action. 22 | /// 23 | /// Name of the method. 24 | /// The handler action with arguments. 25 | public void On(string methodName, Action handler) 26 | { 27 | var invocationHandler = new InvocationHandler(handler, new Type[] { }); 28 | _handlers.Add(methodName, invocationHandler); 29 | } 30 | 31 | /// 32 | /// Registers the specified method name and calls the function. 33 | /// 34 | /// Name of the method. 35 | /// The handler function with arguments and return value. 36 | public void On(string methodName, Func handler) 37 | { 38 | var invocationHandler = new InvocationHandler(handler, new Type[] { }); 39 | _handlers.Add(methodName, invocationHandler); 40 | } 41 | 42 | private class InvocationHandler 43 | { 44 | public Func Handler { get; set; } 45 | public Type[] ParameterTypes { get; set; } 46 | 47 | public InvocationHandler(Func handler, Type[] parameterTypes) 48 | { 49 | Handler = handler; 50 | ParameterTypes = parameterTypes; 51 | } 52 | 53 | public InvocationHandler(Action handler, Type[] parameterTypes) 54 | { 55 | Handler = (args) => { handler(args); return null; }; 56 | ParameterTypes = parameterTypes; 57 | } 58 | } 59 | 60 | /// 61 | /// Called when an invoke method call has been received. 62 | /// 63 | /// The web-socket of the client that wants to invoke a method. 64 | /// 65 | /// The invocation descriptor containing the method name and parameters. 66 | /// 67 | /// Awaitable Task. 68 | public override async Task OnInvokeMethodReceivedAsync(WebSocket socket, InvocationDescriptor invocationDescriptor) 69 | { 70 | if (!_handlers.ContainsKey(invocationDescriptor.MethodName)) 71 | throw new Exception($"Received unknown command '{invocationDescriptor.MethodName}'."); 72 | var invocationHandler = _handlers[invocationDescriptor.MethodName]; 73 | if (invocationHandler != null) 74 | return await Task.Run(() => invocationHandler.Handler(invocationDescriptor.Arguments)); 75 | return await Task.FromResult(null); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/WebSocketManager.Common/WebSocketManager.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 0.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/WebSocketManager/WebSocketConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.WebSockets; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebSocketManager 10 | { 11 | public class WebSocketConnectionManager 12 | { 13 | private ConcurrentDictionary _sockets = new ConcurrentDictionary(); 14 | private ConcurrentDictionary> _groups = new ConcurrentDictionary>(); 15 | 16 | public WebSocket GetSocketById(string id) 17 | { 18 | _sockets.TryGetValue(id, out var socket); 19 | return socket; 20 | } 21 | 22 | public ConcurrentDictionary GetAll() 23 | { 24 | return _sockets; 25 | } 26 | 27 | public List GetAllFromGroup(string GroupID) 28 | { 29 | if (_groups.ContainsKey(GroupID)) 30 | { 31 | return _groups[GroupID]; 32 | } 33 | 34 | return default(List); 35 | } 36 | 37 | public string GetId(WebSocket socket) 38 | { 39 | return _sockets.FirstOrDefault(p => p.Value == socket).Key; 40 | } 41 | 42 | public void AddSocket(WebSocket socket) 43 | { 44 | _sockets.TryAdd(CreateConnectionId(), socket); 45 | } 46 | 47 | public void AddToGroup(string socketID, string groupID) 48 | { 49 | if (_groups.ContainsKey(groupID)) 50 | { 51 | _groups[groupID].Add(socketID); 52 | 53 | return; 54 | } 55 | 56 | _groups.TryAdd(groupID, new List { socketID }); 57 | } 58 | 59 | public void RemoveFromGroup(string socketID, string groupID) 60 | { 61 | if (_groups.ContainsKey(groupID)) 62 | { 63 | _groups[groupID].Remove(socketID); 64 | } 65 | } 66 | 67 | public async Task RemoveSocket(string id) 68 | { 69 | if (id == null) return; 70 | 71 | _sockets.TryRemove(id, out var socket); 72 | 73 | if (socket.State != WebSocketState.Open) return; 74 | 75 | await socket.CloseAsync(closeStatus: WebSocketCloseStatus.NormalClosure, 76 | statusDescription: "Closed by the WebSocketManager", 77 | cancellationToken: CancellationToken.None).ConfigureAwait(false); 78 | } 79 | 80 | private string CreateConnectionId() 81 | { 82 | return Guid.NewGuid().ToString(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/WebSocketManager/WebSocketHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Reflection; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Serialization; 9 | using WebSocketManager.Common; 10 | using System.Collections.Generic; 11 | 12 | namespace WebSocketManager 13 | { 14 | public abstract class WebSocketHandler 15 | { 16 | protected WebSocketConnectionManager WebSocketConnectionManager { get; set; } 17 | 18 | private JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings() 19 | { 20 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 21 | TypeNameHandling = TypeNameHandling.All, 22 | TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, 23 | SerializationBinder = new JsonBinderWithoutAssembly() 24 | }; 25 | 26 | /// 27 | /// The waiting remote invocations for Server to Client method calls. 28 | /// 29 | private Dictionary> _waitingRemoteInvocations = new Dictionary>(); 30 | 31 | /// 32 | /// Gets the method invocation strategy. 33 | /// 34 | /// The method invocation strategy. 35 | public MethodInvocationStrategy MethodInvocationStrategy { get; } 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The web socket connection manager. 41 | /// The method invocation strategy used for incoming requests. 42 | public WebSocketHandler(WebSocketConnectionManager webSocketConnectionManager, MethodInvocationStrategy methodInvocationStrategy) 43 | { 44 | _jsonSerializerSettings.Converters.Insert(0, new PrimitiveJsonConverter()); 45 | WebSocketConnectionManager = webSocketConnectionManager; 46 | MethodInvocationStrategy = methodInvocationStrategy; 47 | } 48 | 49 | /// 50 | /// Called when a client has connected to the server. 51 | /// 52 | /// The web-socket of the client. 53 | /// Awaitable Task. 54 | public virtual async Task OnConnected(WebSocket socket) 55 | { 56 | WebSocketConnectionManager.AddSocket(socket); 57 | 58 | await SendMessageAsync(socket, new Message() 59 | { 60 | MessageType = MessageType.ConnectionEvent, 61 | Data = WebSocketConnectionManager.GetId(socket) 62 | }).ConfigureAwait(false); 63 | } 64 | 65 | /// 66 | /// Called when a client has disconnected from the server. 67 | /// 68 | /// The web-socket of the client. 69 | /// Awaitable Task. 70 | public virtual async Task OnDisconnected(WebSocket socket) 71 | { 72 | await WebSocketConnectionManager.RemoveSocket(WebSocketConnectionManager.GetId(socket)).ConfigureAwait(false); 73 | } 74 | 75 | public async Task SendMessageAsync(WebSocket socket, Message message) 76 | { 77 | if (socket.State != WebSocketState.Open) 78 | return; 79 | 80 | var serializedMessage = JsonConvert.SerializeObject(message, _jsonSerializerSettings); 81 | var encodedMessage = Encoding.UTF8.GetBytes(serializedMessage); 82 | await socket.SendAsync(buffer: new ArraySegment(array: encodedMessage, 83 | offset: 0, 84 | count: encodedMessage.Length), 85 | messageType: WebSocketMessageType.Text, 86 | endOfMessage: true, 87 | cancellationToken: CancellationToken.None).ConfigureAwait(false); 88 | } 89 | 90 | public async Task SendMessageAsync(string socketId, Message message) 91 | { 92 | await SendMessageAsync(WebSocketConnectionManager.GetSocketById(socketId), message).ConfigureAwait(false); 93 | } 94 | 95 | public async Task SendMessageToAllAsync(Message message) 96 | { 97 | foreach (var pair in WebSocketConnectionManager.GetAll()) 98 | { 99 | try 100 | { 101 | if (pair.Value.State == WebSocketState.Open) 102 | await SendMessageAsync(pair.Value, message).ConfigureAwait(false); 103 | } 104 | catch (WebSocketException e) 105 | { 106 | if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) 107 | { 108 | await OnDisconnected(pair.Value); 109 | } 110 | } 111 | } 112 | } 113 | 114 | public async Task InvokeClientMethodAsync(string socketId, string methodName, object[] arguments) 115 | { 116 | var message = new Message() 117 | { 118 | MessageType = MessageType.MethodInvocation, 119 | Data = JsonConvert.SerializeObject(new InvocationDescriptor() 120 | { 121 | MethodName = methodName, 122 | Arguments = arguments 123 | }, _jsonSerializerSettings) 124 | }; 125 | 126 | await SendMessageAsync(socketId, message).ConfigureAwait(false); 127 | } 128 | 129 | public async Task InvokeClientMethodAsync(string socketId, string methodName, object[] arguments) 130 | { 131 | // create the method invocation descriptor. 132 | InvocationDescriptor invocationDescriptor = new InvocationDescriptor { MethodName = methodName, Arguments = arguments }; 133 | 134 | // generate a unique identifier for this invocation. 135 | invocationDescriptor.Identifier = Guid.NewGuid(); 136 | 137 | // add ourselves to the waiting list for return values. 138 | TaskCompletionSource task = new TaskCompletionSource(); 139 | // after a timeout of 60 seconds we will cancel the task and remove it from the waiting list. 140 | new CancellationTokenSource(1000 * 60).Token.Register(() => { _waitingRemoteInvocations.Remove(invocationDescriptor.Identifier); task.TrySetCanceled(); }); 141 | _waitingRemoteInvocations.Add(invocationDescriptor.Identifier, task); 142 | 143 | // send the method invocation to the client. 144 | var message = new Message() { MessageType = MessageType.MethodInvocation, Data = JsonConvert.SerializeObject(invocationDescriptor, _jsonSerializerSettings) }; 145 | await SendMessageAsync(socketId, message).ConfigureAwait(false); 146 | 147 | // wait for the return value elsewhere in the program. 148 | InvocationResult result = await task.Task; 149 | 150 | // ... we just got an answer. 151 | 152 | // if we have completed successfully: 153 | if (task.Task.IsCompleted) 154 | { 155 | // there was a remote exception so we throw it here. 156 | if (result.Exception != null) 157 | throw new Exception(result.Exception.Message); 158 | 159 | // return the value. 160 | 161 | // support null. 162 | if (result.Result == null) return default(T); 163 | // cast anything to T and hope it works. 164 | return (T)result.Result; 165 | } 166 | 167 | // if we reach here we got cancelled or alike so throw a timeout exception. 168 | throw new TimeoutException(); // todo: insert fancy message here. 169 | } 170 | 171 | public async Task InvokeClientMethodToAllAsync(string methodName, params object[] arguments) 172 | { 173 | foreach (var pair in WebSocketConnectionManager.GetAll()) 174 | { 175 | try 176 | { 177 | if (pair.Value.State == WebSocketState.Open) 178 | await InvokeClientMethodAsync(pair.Key, methodName, arguments).ConfigureAwait(false); 179 | } 180 | catch (WebSocketException e) 181 | { 182 | if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) 183 | { 184 | await OnDisconnected(pair.Value); 185 | } 186 | } 187 | } 188 | } 189 | 190 | public async Task SendMessageToGroupAsync(string groupID, Message message) 191 | { 192 | var sockets = WebSocketConnectionManager.GetAllFromGroup(groupID); 193 | if (sockets != null) 194 | { 195 | foreach (var socket in sockets) 196 | { 197 | await SendMessageAsync(socket, message); 198 | } 199 | } 200 | } 201 | 202 | public async Task SendMessageToGroupAsync(string groupID, Message message, string except) 203 | { 204 | var sockets = WebSocketConnectionManager.GetAllFromGroup(groupID); 205 | if (sockets != null) 206 | { 207 | foreach (var id in sockets) 208 | { 209 | if (id != except) 210 | await SendMessageAsync(id, message); 211 | } 212 | } 213 | } 214 | 215 | public async Task InvokeClientMethodToGroupAsync(string groupID, string methodName, params object[] arguments) 216 | { 217 | var sockets = WebSocketConnectionManager.GetAllFromGroup(groupID); 218 | if (sockets != null) 219 | { 220 | foreach (var id in sockets) 221 | { 222 | await InvokeClientMethodAsync(id, methodName, arguments); 223 | } 224 | } 225 | } 226 | 227 | public async Task InvokeClientMethodToGroupAsync(string groupID, string methodName, string except, params object[] arguments) 228 | { 229 | var sockets = WebSocketConnectionManager.GetAllFromGroup(groupID); 230 | if (sockets != null) 231 | { 232 | foreach (var id in sockets) 233 | { 234 | if (id != except) 235 | await InvokeClientMethodAsync(id, methodName, arguments); 236 | } 237 | } 238 | } 239 | 240 | public async Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, Message receivedMessage) 241 | { 242 | // method invocation request. 243 | if (receivedMessage.MessageType == MessageType.MethodInvocation) 244 | { 245 | // retrieve the method invocation request. 246 | InvocationDescriptor invocationDescriptor = null; 247 | try 248 | { 249 | invocationDescriptor = JsonConvert.DeserializeObject(receivedMessage.Data, _jsonSerializerSettings); 250 | if (invocationDescriptor == null) return; 251 | } 252 | catch { return; } // ignore invalid data sent to the server. 253 | 254 | // if the unique identifier hasn't been set then the client doesn't want a return value. 255 | if (invocationDescriptor.Identifier == Guid.Empty) 256 | { 257 | // invoke the method only. 258 | try 259 | { 260 | await MethodInvocationStrategy.OnInvokeMethodReceivedAsync(socket, invocationDescriptor); 261 | } 262 | catch (Exception) 263 | { 264 | // we consume all exceptions. 265 | } 266 | } 267 | else 268 | { 269 | // invoke the method and get the result. 270 | InvocationResult invokeResult; 271 | try 272 | { 273 | // create an invocation result with the results. 274 | invokeResult = new InvocationResult() 275 | { 276 | Identifier = invocationDescriptor.Identifier, 277 | Result = await MethodInvocationStrategy.OnInvokeMethodReceivedAsync(socket, invocationDescriptor), 278 | Exception = null 279 | }; 280 | } 281 | // send the exception as the invocation result if there was one. 282 | catch (Exception ex) 283 | { 284 | invokeResult = new InvocationResult() 285 | { 286 | Identifier = invocationDescriptor.Identifier, 287 | Result = null, 288 | Exception = new RemoteException(ex) 289 | }; 290 | } 291 | 292 | // send a message to the client containing the result. 293 | var message = new Message() 294 | { 295 | MessageType = MessageType.MethodReturnValue, 296 | Data = JsonConvert.SerializeObject(invokeResult, _jsonSerializerSettings) 297 | }; 298 | await SendMessageAsync(socket, message).ConfigureAwait(false); 299 | } 300 | } 301 | 302 | // method return value. 303 | else if (receivedMessage.MessageType == MessageType.MethodReturnValue) 304 | { 305 | var invocationResult = JsonConvert.DeserializeObject(receivedMessage.Data, _jsonSerializerSettings); 306 | // find the completion source in the waiting list. 307 | if (_waitingRemoteInvocations.ContainsKey(invocationResult.Identifier)) 308 | { 309 | // set the result of the completion source so the invoke method continues executing. 310 | _waitingRemoteInvocations[invocationResult.Identifier].SetResult(invocationResult); 311 | // remove the completion source from the waiting list. 312 | _waitingRemoteInvocations.Remove(invocationResult.Identifier); 313 | } 314 | } 315 | } 316 | 317 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method) => await InvokeClientMethodAsync(socketId, method, new object[] { }); 318 | 319 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1 }); 320 | 321 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2 }); 322 | 323 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3 }); 324 | 325 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4 }); 326 | 327 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5 }); 328 | 329 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6 }); 330 | 331 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7 }); 332 | 333 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 }); 334 | 335 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 }); 336 | 337 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 }); 338 | 339 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 }); 340 | 341 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12 }); 342 | 343 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13 }); 344 | 345 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14 }); 346 | 347 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15 }); 348 | 349 | public async Task InvokeClientMethodOnlyAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16 }); 350 | 351 | public async Task InvokeClientMethodAsync(string socketId, string method) => await InvokeClientMethodAsync(socketId, method, new object[] { }); 352 | 353 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1 }); 354 | 355 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2 }); 356 | 357 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3 }); 358 | 359 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4 }); 360 | 361 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5 }); 362 | 363 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6 }); 364 | 365 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7 }); 366 | 367 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 }); 368 | 369 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 }); 370 | 371 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 }); 372 | 373 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 }); 374 | 375 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12 }); 376 | 377 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13 }); 378 | 379 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14 }); 380 | 381 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15 }); 382 | 383 | public async Task InvokeClientMethodAsync(string socketId, string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16) => await InvokeClientMethodAsync(socketId, method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16 }); 384 | 385 | public async Task InvokeClientMethodToAllAsync(string method) => await InvokeClientMethodToAllAsync(method, new object[] { }); 386 | 387 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1) => await InvokeClientMethodToAllAsync(method, new object[] { arg1 }); 388 | 389 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2 }); 390 | 391 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3 }); 392 | 393 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4 }); 394 | 395 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5 }); 396 | 397 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6 }); 398 | 399 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7 }); 400 | 401 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 }); 402 | 403 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 }); 404 | 405 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 }); 406 | 407 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 }); 408 | 409 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12 }); 410 | 411 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13 }); 412 | 413 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14 }); 414 | 415 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15 }); 416 | 417 | public async Task InvokeClientMethodToAllAsync(string method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16) => await InvokeClientMethodToAllAsync(method, new object[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16 }); 418 | } 419 | } -------------------------------------------------------------------------------- /src/WebSocketManager/WebSocketManager.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 0.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/WebSocketManager/WebSocketManagerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace WebSocketManager 7 | { 8 | public static class WebSocketManagerExtensions 9 | { 10 | public static IServiceCollection AddWebSocketManager(this IServiceCollection services, Assembly assembly = null) 11 | { 12 | services.AddTransient(); 13 | 14 | Assembly ass = assembly ?? Assembly.GetEntryAssembly(); 15 | 16 | foreach (var type in ass.ExportedTypes) 17 | { 18 | if (type.GetTypeInfo().BaseType == typeof(WebSocketHandler)) 19 | { 20 | services.AddSingleton(type); 21 | } 22 | } 23 | 24 | return services; 25 | } 26 | 27 | public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app, 28 | PathString path, 29 | WebSocketHandler handler) 30 | { 31 | return app.Map(path, (_app) => _app.UseMiddleware(handler)); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/WebSocketManager/WebSocketManagerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.WebSockets; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Serialization; 10 | using WebSocketManager.Common; 11 | 12 | namespace WebSocketManager 13 | { 14 | public class WebSocketManagerMiddleware 15 | { 16 | private readonly RequestDelegate _next; 17 | private WebSocketHandler _webSocketHandler { get; set; } 18 | 19 | private JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings() 20 | { 21 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 22 | TypeNameHandling = TypeNameHandling.All, 23 | TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, 24 | SerializationBinder = new JsonBinderWithoutAssembly() 25 | }; 26 | 27 | public WebSocketManagerMiddleware(RequestDelegate next, 28 | WebSocketHandler webSocketHandler) 29 | { 30 | _jsonSerializerSettings.Converters.Insert(0, new PrimitiveJsonConverter()); 31 | _next = next; 32 | _webSocketHandler = webSocketHandler; 33 | } 34 | 35 | public async Task Invoke(HttpContext context) 36 | { 37 | if (!context.WebSockets.IsWebSocketRequest) 38 | { 39 | await _next.Invoke(context); 40 | return; 41 | } 42 | 43 | var socket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); 44 | await _webSocketHandler.OnConnected(socket).ConfigureAwait(false); 45 | 46 | await Receive(socket, async (result, serializedMessage) => 47 | { 48 | if (result.MessageType == WebSocketMessageType.Text) 49 | { 50 | Message message = JsonConvert.DeserializeObject(serializedMessage, _jsonSerializerSettings); 51 | await _webSocketHandler.ReceiveAsync(socket, result, message).ConfigureAwait(false); 52 | return; 53 | } 54 | else if (result.MessageType == WebSocketMessageType.Close) 55 | { 56 | try 57 | { 58 | await _webSocketHandler.OnDisconnected(socket); 59 | } 60 | catch (WebSocketException) 61 | { 62 | throw; //let's not swallow any exception for now 63 | } 64 | 65 | return; 66 | } 67 | }); 68 | } 69 | 70 | private async Task Receive(WebSocket socket, Action handleMessage) 71 | { 72 | while (socket.State == WebSocketState.Open) 73 | { 74 | ArraySegment buffer = new ArraySegment(new Byte[1024 * 4]); 75 | string message = null; 76 | WebSocketReceiveResult result = null; 77 | try 78 | { 79 | using (var ms = new MemoryStream()) 80 | { 81 | do 82 | { 83 | result = await socket.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); 84 | ms.Write(buffer.Array, buffer.Offset, result.Count); 85 | } 86 | while (!result.EndOfMessage); 87 | 88 | ms.Seek(0, SeekOrigin.Begin); 89 | 90 | using (var reader = new StreamReader(ms, Encoding.UTF8)) 91 | { 92 | message = await reader.ReadToEndAsync().ConfigureAwait(false); 93 | } 94 | } 95 | 96 | handleMessage(result, message); 97 | } 98 | catch (WebSocketException e) 99 | { 100 | if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) 101 | { 102 | socket.Abort(); 103 | } 104 | } 105 | } 106 | 107 | await _webSocketHandler.OnDisconnected(socket); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /test/WebSocketManager.Tests/Helpers/FakeSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace WebSocketManager.Tests.Helpers 7 | { 8 | internal class FakeSocket : WebSocket 9 | { 10 | public override void Abort() 11 | { 12 | throw new NotImplementedException(); 13 | } 14 | 15 | public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) 16 | { 17 | throw new NotImplementedException(); 18 | } 19 | 20 | public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) 21 | { 22 | throw new NotImplementedException(); 23 | } 24 | 25 | public override void Dispose() 26 | { 27 | throw new NotImplementedException(); 28 | } 29 | 30 | public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) 31 | { 32 | throw new NotImplementedException(); 33 | } 34 | 35 | public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) 36 | { 37 | throw new NotImplementedException(); 38 | } 39 | 40 | public override WebSocketCloseStatus? CloseStatus { get; } 41 | 42 | public override string CloseStatusDescription { get; } 43 | 44 | public override WebSocketState State { get; } 45 | 46 | public override string SubProtocol { get; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/WebSocketManager.Tests/WebSocketConnectionManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using WebSocketManager.Tests.Helpers; 3 | using Xunit; 4 | 5 | namespace WebSocketManager.Tests 6 | { 7 | public class WebSocketConnectionManagerTests 8 | { 9 | private readonly WebSocketConnectionManager _manager; 10 | 11 | public WebSocketConnectionManagerTests() 12 | { 13 | _manager = new WebSocketConnectionManager(); 14 | } 15 | 16 | public class GetSocketById : WebSocketConnectionManagerTests 17 | { 18 | [Theory] 19 | [InlineData(null)] 20 | [InlineData("")] 21 | [InlineData("foo")] 22 | public void WhenNonExistentId_ShouldReturnNull(string id) 23 | { 24 | var socket = _manager.GetSocketById(id); 25 | 26 | Assert.Null(socket); 27 | } 28 | 29 | [Fact] 30 | public void WhenExistingId_ShouldReturnSocket() 31 | { 32 | var socket = new FakeSocket(); 33 | 34 | _manager.AddSocket(socket); 35 | var id = _manager.GetId(socket); 36 | 37 | Assert.Same(socket, _manager.GetSocketById(id)); 38 | } 39 | } 40 | 41 | public class GetAll : WebSocketConnectionManagerTests 42 | { 43 | [Fact] 44 | public void WhenEmpty_ShouldReturnZero() 45 | { 46 | Assert.Equal(0, _manager.GetAll().Count); 47 | } 48 | 49 | [Fact] 50 | public void WhenOneSocket_ShouldReturnOne() 51 | { 52 | _manager.AddSocket(new FakeSocket()); 53 | 54 | Assert.Equal(1, _manager.GetAll().Count); 55 | } 56 | } 57 | 58 | public class GetAllFromGroup : WebSocketConnectionManagerTests 59 | { 60 | private string GroupName = "FakeGroup"; 61 | 62 | [Fact] 63 | public void WhenNonExistingGroup_ShouldReturnNull() 64 | { 65 | Assert.Null(_manager.GetAllFromGroup(GroupName)); 66 | } 67 | 68 | [Fact] 69 | public void WhenOneSocketInGroup_ShouldReturnOne() 70 | { 71 | var socket = new FakeSocket(); 72 | _manager.AddSocket(socket); 73 | var socketID = _manager.GetId(socket); 74 | _manager.AddToGroup(socketID, GroupName); 75 | 76 | Assert.Equal(1, _manager.GetAllFromGroup(GroupName).Count); 77 | } 78 | } 79 | 80 | public class GetId : WebSocketConnectionManagerTests 81 | { 82 | [Fact] 83 | public void WhenNull_ShouldReturnNull() 84 | { 85 | var id = _manager.GetId(null); 86 | 87 | Assert.Null(id); 88 | } 89 | 90 | [Fact] 91 | public void WhenUntrackedInstance_ShouldReturnNull() 92 | { 93 | var id = _manager.GetId(new FakeSocket()); 94 | 95 | Assert.Null(id); 96 | } 97 | 98 | [Fact] 99 | public void WhenTrackedInstance_ShouldReturnId() 100 | { 101 | var socket = new FakeSocket(); 102 | _manager.AddSocket(socket); 103 | 104 | var id = _manager.GetId(socket); 105 | 106 | Assert.NotNull(id); 107 | } 108 | } 109 | 110 | public class AddSocket : WebSocketConnectionManagerTests 111 | { 112 | [Fact(Skip = "At the moment the implementation allows adding null references")] 113 | public void WhenNull_ShouldNotNotContainSocket() 114 | { 115 | _manager.AddSocket(null); 116 | 117 | Assert.Equal(0, _manager.GetAll().Count); 118 | } 119 | 120 | [Fact] 121 | public void WhenInstance_ShouldContainSocket() 122 | { 123 | _manager.AddSocket(new FakeSocket()); 124 | 125 | Assert.Equal(1, _manager.GetAll().Count); 126 | } 127 | } 128 | 129 | public class RemoveSocket : WebSocketConnectionManagerTests 130 | { 131 | [Theory(Skip = "Currently it doesn't check if the socket was removed or not, so we get an NRE")] 132 | [InlineData(null)] 133 | [InlineData("")] 134 | [InlineData("foo")] 135 | public async Task WhenNonExistentId_ShouldNotThrowException(string id) 136 | { 137 | await _manager.RemoveSocket(id); 138 | } 139 | } 140 | 141 | public class RemoveFromGroup : WebSocketConnectionManagerTests 142 | { 143 | private string GroupName = "FakeGroup"; 144 | 145 | [Theory(Skip = "Currently it doesn't check for non existing sockets")] 146 | public void WhenRemoveNonExisting_ShouldNotThrowException() 147 | { 148 | _manager.RemoveFromGroup("", GroupName); 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /test/WebSocketManager.Tests/WebSocketManager.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | 0.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------