├── images
└── csharp-websockets-chat-demo.gif
├── Comm
├── Global.cs
├── Client.cs
└── Server.cs
├── conf.ex.json
├── Utils
└── Utils.cs
├── csharp-websockets.csproj
├── csharp-websockets.sln
├── Cmd
└── Cmd.cs
├── README.md
├── Config
└── Config.cs
├── .gitignore
└── Program.cs
/images/csharp-websockets-chat-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gamemann/csharp-websockets-chat/HEAD/images/csharp-websockets-chat-demo.gif
--------------------------------------------------------------------------------
/Comm/Global.cs:
--------------------------------------------------------------------------------
1 | namespace Program.Comm {
2 | public struct Flow {
3 | public string host;
4 | public ushort port;
5 | }
6 | }
--------------------------------------------------------------------------------
/conf.ex.json:
--------------------------------------------------------------------------------
1 | {
2 | "listen": true,
3 | "listenHost": "127.0.0.1",
4 | "listenPort": 2222,
5 | "listenSsl": false,
6 | "startupConnections": [
7 | ]
8 | }
--------------------------------------------------------------------------------
/Utils/Utils.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace Program {
4 | public class Utils {
5 | public static bool IsValidIpv4(string ip) {
6 | try {
7 | IPAddress.Parse(ip);
8 | } catch (Exception e) {
9 | Console.WriteLine($"Invalid IP '{ip}'.");
10 | Console.WriteLine(e);
11 |
12 | return false;
13 | }
14 |
15 | return true;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/csharp-websockets.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | csharp_websockets
7 | enable
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/csharp-websockets.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.002.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp-websockets", "csharp-websockets.csproj", "{AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {62162987-7D88-4599-83AC-E8FB59A9DBAE}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Cmd/Cmd.cs:
--------------------------------------------------------------------------------
1 | using CommandLine;
2 |
3 | namespace Program {
4 | public class Cmd {
5 | public class Options {
6 | [Option('z', "cfg", Required = false, Default = "./conf.json", HelpText = "The location of the config file.")]
7 | public string? Cfg { set; get; }
8 |
9 | [Option(longName:"nolisten", Required = false, Default = false, HelpText = "Disables the listen server.")]
10 | public bool NoListen { get; set; }
11 |
12 | [Option(longName:"host", Required = false, Default = null, HelpText = "The host to listen on. Overrides config.")]
13 | public string? Host { get; set; }
14 |
15 | [Option(longName:"port", Required = false, Default = null, HelpText = "The port to listen on. Overrides config.")]
16 | public int? Port { get; set; }
17 |
18 | [Option(longName:"ssl", Required = false, Default = null, HelpText = "Whether to listen with SSL. Overrides config.")]
19 | public bool? Ssl { get; set; }
20 |
21 | [Option('l', "list", Required = false, Default = false, HelpText = "Whether to print config file.")]
22 | public bool List { set; get; }
23 | }
24 |
25 | private Options opts = new();
26 | public Options Opts {
27 | get => opts;
28 | set => opts = value;
29 | }
30 |
31 | public void Parse(string[] args) {
32 | Parser.Default.ParseArguments(args).WithParsed((o) => {
33 | opts = o;
34 | });
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/Comm/Client.cs:
--------------------------------------------------------------------------------
1 | using System.Net.WebSockets;
2 | using System.Text;
3 |
4 | namespace Program.Comm {
5 | public class Client {
6 | private Task? task = null;
7 | public Task? Task {
8 | get => task;
9 | set => task = value;
10 | }
11 |
12 | private bool ssl = true;
13 | public bool Ssl {
14 | get => ssl;
15 | set => ssl = value;
16 | }
17 |
18 | private Flow server = new();
19 | public Flow Server {
20 | get => server;
21 | set => server = value;
22 | }
23 |
24 | private ClientWebSocket ws = new();
25 | public ClientWebSocket Ws {
26 | get => ws;
27 | set => ws = value;
28 | }
29 |
30 | public async Task Connect() {
31 | // Determine the protocol to use based off of SSL option.
32 | var protocol = "ws";
33 |
34 | if (ssl)
35 | protocol = "wss";
36 |
37 | var uri = new Uri($"{protocol}://{server.host}:{server.port}");
38 |
39 | await ws.ConnectAsync(uri, CancellationToken.None);
40 | }
41 |
42 | public async Task Send(string msg) {
43 | if (ws.State != WebSocketState.Open)
44 | throw new Exception("Failed to send message to server. Web socket state is not open.");
45 |
46 | var buffer = Encoding.UTF8.GetBytes(msg);
47 |
48 | await ws.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
49 | }
50 |
51 | public async Task Recv() {
52 | if (ws.State != WebSocketState.Open)
53 | throw new Exception("Failed to send receive message from server. Web socket state is not open.");
54 |
55 | var recvBuffer = new byte[2048];
56 |
57 | var recvRes = await ws.ReceiveAsync(new ArraySegment(recvBuffer), CancellationToken.None);
58 |
59 | var msg = Encoding.UTF8.GetString(recvBuffer, 0, recvRes.Count);
60 |
61 | return msg;
62 | }
63 |
64 | public async Task Disconnect() {
65 | if (ws.State != WebSocketState.Open && ws.State != WebSocketState.Connecting)
66 | throw new Exception("Failed to disconnect client session. Web socket is not open or connecting.");
67 |
68 | await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/Comm/Server.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.WebSockets;
3 | using System.Text;
4 |
5 | namespace Program.Comm {
6 | public class Server {
7 | private bool ssl = true;
8 | public bool Ssl {
9 | get => ssl;
10 | set => ssl = value;
11 | }
12 |
13 | private Flow bind = new();
14 | public Flow Bind {
15 | get => bind;
16 | set => bind = value;
17 | }
18 |
19 | private HttpListener listener = new();
20 | public HttpListener Listener {
21 | get => listener;
22 | set => listener = value;
23 | }
24 |
25 | private WebSocket? ws = null;
26 | public WebSocket? Ws {
27 | get => ws;
28 | set => ws = value;
29 | }
30 |
31 | public void Listen() {
32 | // Figure out the protocol we're using.
33 | var protocol = "http";
34 |
35 | if (ssl)
36 | protocol = "https";
37 |
38 | // Create HTTP listener.
39 | listener.Prefixes.Add($"{protocol}://{bind.host}:{bind.port}/");
40 |
41 | listener.Start();
42 | }
43 |
44 | public async Task Send(string msg) {
45 | if (ws == null || ws.State != WebSocketState.Open)
46 | throw new Exception("Failed to send message. Web socket is null.");
47 |
48 | var buffer = Encoding.UTF8.GetBytes(msg);
49 |
50 | await ws.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
51 | }
52 |
53 | public async Task Recv() {
54 | if (ws == null || ws.State != WebSocketState.Open)
55 | throw new Exception("Failed to receive message. Web socket is null.");
56 |
57 | var recvBuffer = new byte[2048];
58 |
59 | var recvRes = await ws.ReceiveAsync(new ArraySegment(recvBuffer), CancellationToken.None);
60 |
61 | if (recvRes.MessageType == WebSocketMessageType.Text) {
62 | var msg = Encoding.UTF8.GetString(recvBuffer, 0, recvRes.Count);
63 |
64 | return msg;
65 | } else if (recvRes.MessageType == WebSocketMessageType.Close) {
66 | await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
67 | }
68 |
69 | return null;
70 | }
71 |
72 | public async Task Disconnect() {
73 | if (ws == null || (ws.State != WebSocketState.Open && ws.State != WebSocketState.Connecting))
74 | throw new Exception("Failed to disconnect server session. Web socket is not open or connecting.");
75 |
76 | await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a small project that utilizes CSharp and the [.NET library](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet) (7.0). This program allows you to establish multiple web sockets at once (client -> server and server -> client both supported). This is intended to run on Linux-based operating systems that support .NET 7.0 (e.g. using the `dotnet` package). It is possible this works with Windows, but I haven't tried testing it. This project should also work with .NET 8.0, but I haven't tested that as well.
2 |
3 | This program operates as a very simple one-on-one chat room. When connected, the client and server can exchange basic UTF-8 text messages with each other.
4 |
5 | I made this project to learn more about web sockets in CSharp/.NET along with how to manage multiple web sockets receiving/sending data concurrently via asynchronous methods.
6 |
7 | ## Demo
8 | Here's a GIF video demonstrating the functionality of the program. We don't use SSL in our demonstration and establish the client and server both locally using `127.0.0.1` (localhost).
9 |
10 | 
11 |
12 | ## Building & Installing
13 | ### Prerequisites
14 | #### .NET 7.0
15 | The .NET 7.0 library is required to run this project. You can install this library manually or through a package manager if your Linux distro supports it.
16 |
17 | On Ubuntu/Debian-based systems, you may install Dotnet using the below command(s).
18 |
19 | ```bash
20 | # Typically this is only required for Debian.
21 | wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
22 | sudo dpkg -i packages-microsoft-prod.deb
23 | rm packages-microsoft-prod.deb
24 | sudo apt update
25 |
26 | # Install .NET 7.0.
27 | sudo apt install -y dotnet-sdk-7.0
28 | ```
29 |
30 | #### Building & Running
31 | If you want to build and run the project. You can use the following command.
32 |
33 | ```bash
34 | dotnet run
35 | ```
36 |
37 | If you want to only build the project, you can use the following command.
38 |
39 | ```bash
40 | dotnet build
41 | ```
42 |
43 | Make sure you're in the same directory as the `csharp-websockets.csproj` file when performing the above commands.
44 |
45 | ## Command Line
46 | The following command line arguments are supported.
47 |
48 | * **-z --cfg** => The path to the config file. By default, it looks for `./conf.json`.
49 | * **--nolisten** => Prevents the listen server from activating.
50 | * **--host** => Overrides the host address to listen on.
51 | * **--port** => Overrides the port to listen on.
52 | * **--ssl** => Overrides the listen SSL option.
53 | * **-l --list** => Lists all values of config and exits.
54 |
55 | ## Configuration
56 | A config file on the file system is read and parsed via the JSON syntax. The default path it checks for is `./conf.json`. However, it can be changed via the config path command line option listed above.
57 |
58 | Here are the config options. Please keep in mind you will need to remove the comments (`//`) if copying below. I recommend taking a look at the [conf.ex.json](./conf.ex.json) file if you want to copy the configuration without any errors.
59 |
60 | ```
61 | {
62 | // Whether to activate the listen server.
63 | "listen": true,
64 |
65 | // The host to listen on.
66 | "listenHost": "127.0.0.1",
67 |
68 | // The port to listen on.
69 | "listenPort": 2222,
70 |
71 | // Whether to listen with SSL.
72 | "listenSsl": false,
73 |
74 | // An array of startup client connections.
75 | "startupConnections": [
76 | {
77 | // The startup connection host.
78 | "host": "127.0.0.1",
79 |
80 | // The startup connection port.
81 | "port": 2223,
82 |
83 | // Whether to use SSL with the startup connection.
84 | "ssl": false
85 | }
86 | ]
87 | }
88 | ```
89 |
90 | ## Usage
91 | When starting up the program, you will be prompted with the following commands you can use.
92 |
93 | * **ls** - List all server connections. The number at the beginning represents the index which should be used with other commands.
94 | * **lc** - List all client connections. The number at the beginning represents the index which should be used with other commands.
95 | * **new `` `` ``** - Establish a new client connection to ``:``. `` is optional and to disable SSL, use **no**.
96 | * **cc ``** - Send/receive messages for client connection at index ``.
97 | * **cs ``** - Send/receive messages for listen server at index ``.
98 | * **rc ``** - Remove client at index ``.
99 | * **rs ``** - Remove server at index ``.
100 | * **h** - Print top/help menu.
101 | * **q** - Exit program.
102 |
103 | When connected to a chat session via the `cc` and `cs` commands, you can send the message `\q` to detach the current chat session and return to the main menu.
104 |
105 | ## Notes
106 | * This project is still a work in progress. I have not yet tested SSL.
107 | * Only one connection to the listen server is supported at the moment. New connections will override the previous connection. However, I may add support for multiple connections in the future when I have more time.
108 | * There are some code that can definitely be improved on/organized better.
109 |
110 | ## Credits
111 | * [Christian Deacon](https://github.com/gamemann)
--------------------------------------------------------------------------------
/Config/Config.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Nodes;
2 |
3 | namespace Program {
4 | public class Config {
5 | public struct Server {
6 | public string host;
7 | public ushort port;
8 | }
9 |
10 | public struct Connection {
11 | public Server srv;
12 | public bool ssl;
13 | }
14 |
15 | private bool listen = true;
16 | public bool Listen {
17 | get => listen;
18 | set => listen = value;
19 | }
20 |
21 | private string listenHost = "127.0.0.1";
22 | public string ListenHost {
23 | get => listenHost;
24 | set => listenHost = value;
25 | }
26 |
27 | private ushort listenPort = 2222;
28 | public ushort ListenPort {
29 | get => listenPort;
30 | set => listenPort = value;
31 | }
32 |
33 | private bool listenSsl = false;
34 | public bool ListenSsl {
35 | get => listenSsl;
36 | set => listenSsl = value;
37 | }
38 |
39 | private List startupConnections = new();
40 | public List StartupConnections {
41 | get => startupConnections;
42 | set => startupConnections = value;
43 | }
44 |
45 | public void Load(string path) {
46 | JsonObject? jsonObj;
47 |
48 | // Read JSON string from config file and store in JSON object.
49 | try {
50 | jsonObj = ReadFromFile(path);
51 |
52 | if (jsonObj == null)
53 | throw new Exception("Failed to laod config file. JSON object is null.");
54 | } catch (Exception e) {
55 | throw new Exception($"Failed to load config file due to exception.\n{e}");
56 | }
57 |
58 | // Attmept to load confg values from JSON object.
59 | try {
60 | LoadValues(jsonObj);
61 | } catch (Exception e) {
62 | throw new Exception($"Failed to read config values due to exception.\n{e}");
63 | }
64 | }
65 |
66 | public void Print() {
67 | // Listen settings.
68 | Console.WriteLine($"Listen Enabled => {listen}");
69 | Console.WriteLine($"Listen Address => '{listenHost}'");
70 | Console.WriteLine($"Listen Port => {listenPort}");
71 | Console.WriteLine($"Listen SSL => {listenSsl}");
72 |
73 | // Servers to connect to.
74 | if (startupConnections.Count > 0) {
75 | Console.WriteLine("Startup Connections");
76 |
77 | var i = 0;
78 |
79 | foreach (var conn in startupConnections) {
80 | Console.WriteLine($"\tConnection #{i + 1}");
81 |
82 | Console.WriteLine($"\t\tHost => {conn.srv.host}");
83 | Console.WriteLine($"\t\tPort => {conn.srv.port}");
84 | Console.WriteLine($"\t\tSSL => {conn.ssl}");
85 |
86 | i++;
87 | }
88 | }
89 | }
90 |
91 | private static JsonObject? ReadFromFile(string path) {
92 | JsonObject? data = null;
93 |
94 | try {
95 | // Read config file.
96 | var text = File.ReadAllText(path);
97 |
98 | // Convert JSON to JSON object.
99 | data = JsonNode.Parse(text)?.AsObject();
100 | } catch (Exception e) {
101 | throw new Exception($"Failed to read from config file.\n{e}");
102 | }
103 |
104 | return data;
105 | }
106 |
107 | private void LoadValues (JsonObject data) {
108 | // Check for listen overrides.
109 | var listenStr = data["listen"]?.ToString();
110 |
111 | if (listenStr != null)
112 | listen = Convert.ToBoolean(listenStr);
113 |
114 | var listenHostStr = data["listenHost"]?.ToString();
115 |
116 | if (listenHostStr != null)
117 | listenHost = listenHostStr;
118 |
119 | var listenPortStr = data["listenPort"]?.ToString();
120 |
121 | if (listenPortStr != null)
122 | listenPort = Convert.ToUInt16(listenPortStr);
123 |
124 | var listenSslStr = data["listenSsl"]?.ToString();
125 |
126 | if (listenSslStr != null)
127 | listenSsl = Convert.ToBoolean(listenSslStr);
128 |
129 | // Check for servers.
130 | try {
131 | var startupConnectionsArr = data["startupConnections"]?.AsArray();
132 |
133 | if (startupConnectionsArr != null) {
134 | // Wipe current startup connections.
135 | startupConnections = new();
136 |
137 | foreach (var conn in startupConnectionsArr) {
138 | if (conn == null)
139 | continue;
140 |
141 | Connection newConn = new();
142 |
143 | // Check for server host.
144 | var hostStr = conn["host"]?.ToString();
145 |
146 | if (hostStr != null)
147 | newConn.srv.host = hostStr;
148 |
149 | // Check for server port.
150 | var portStr = conn["port"]?.ToString();
151 |
152 | if (portStr != null)
153 | newConn.srv.port = Convert.ToUInt16(portStr);
154 |
155 | // Check for SSL.
156 | var sslStr = conn["ssl"]?.ToString();
157 |
158 | if (sslStr != null)
159 | newConn.ssl = Convert.ToBoolean(sslStr);
160 |
161 | // Add new connection to startup connections.
162 | startupConnections.Add(newConn);
163 | }
164 | }
165 | } catch (Exception e) {
166 | throw new Exception($"Failed to read startup connections from config file due to exception. Exception:\n{e}");
167 | }
168 | }
169 | }
170 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # Tye
66 | .tye/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.tlog
97 | *.vspscc
98 | *.vssscc
99 | .builds
100 | *.pidb
101 | *.svclog
102 | *.scc
103 |
104 | # Chutzpah Test files
105 | _Chutzpah*
106 |
107 | # Visual C++ cache files
108 | ipch/
109 | *.aps
110 | *.ncb
111 | *.opendb
112 | *.opensdf
113 | *.sdf
114 | *.cachefile
115 | *.VC.db
116 | *.VC.VC.opendb
117 |
118 | # Visual Studio profiler
119 | *.psess
120 | *.vsp
121 | *.vspx
122 | *.sap
123 |
124 | # Visual Studio Trace Files
125 | *.e2e
126 |
127 | # TFS 2012 Local Workspace
128 | $tf/
129 |
130 | # Guidance Automation Toolkit
131 | *.gpState
132 |
133 | # ReSharper is a .NET coding add-in
134 | _ReSharper*/
135 | *.[Rr]e[Ss]harper
136 | *.DotSettings.user
137 |
138 | # TeamCity is a build add-in
139 | _TeamCity*
140 |
141 | # DotCover is a Code Coverage Tool
142 | *.dotCover
143 |
144 | # AxoCover is a Code Coverage Tool
145 | .axoCover/*
146 | !.axoCover/settings.json
147 |
148 | # Coverlet is a free, cross platform Code Coverage Tool
149 | coverage*.json
150 | coverage*.xml
151 | coverage*.info
152 |
153 | # Visual Studio code coverage results
154 | *.coverage
155 | *.coveragexml
156 |
157 | # NCrunch
158 | _NCrunch_*
159 | .*crunch*.local.xml
160 | nCrunchTemp_*
161 |
162 | # MightyMoose
163 | *.mm.*
164 | AutoTest.Net/
165 |
166 | # Web workbench (sass)
167 | .sass-cache/
168 |
169 | # Installshield output folder
170 | [Ee]xpress/
171 |
172 | # DocProject is a documentation generator add-in
173 | DocProject/buildhelp/
174 | DocProject/Help/*.HxT
175 | DocProject/Help/*.HxC
176 | DocProject/Help/*.hhc
177 | DocProject/Help/*.hhk
178 | DocProject/Help/*.hhp
179 | DocProject/Help/Html2
180 | DocProject/Help/html
181 |
182 | # Click-Once directory
183 | publish/
184 |
185 | # Publish Web Output
186 | *.[Pp]ublish.xml
187 | *.azurePubxml
188 | # Note: Comment the next line if you want to checkin your web deploy settings,
189 | # but database connection strings (with potential passwords) will be unencrypted
190 | *.pubxml
191 | *.publishproj
192 |
193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
194 | # checkin your Azure Web App publish settings, but sensitive information contained
195 | # in these scripts will be unencrypted
196 | PublishScripts/
197 |
198 | # NuGet Packages
199 | *.nupkg
200 | # NuGet Symbol Packages
201 | *.snupkg
202 | # The packages folder can be ignored because of Package Restore
203 | **/[Pp]ackages/*
204 | # except build/, which is used as an MSBuild target.
205 | !**/[Pp]ackages/build/
206 | # Uncomment if necessary however generally it will be regenerated when needed
207 | #!**/[Pp]ackages/repositories.config
208 | # NuGet v3's project.json files produces more ignorable files
209 | *.nuget.props
210 | *.nuget.targets
211 |
212 | # Microsoft Azure Build Output
213 | csx/
214 | *.build.csdef
215 |
216 | # Microsoft Azure Emulator
217 | ecf/
218 | rcf/
219 |
220 | # Windows Store app package directories and files
221 | AppPackages/
222 | BundleArtifacts/
223 | Package.StoreAssociation.xml
224 | _pkginfo.txt
225 | *.appx
226 | *.appxbundle
227 | *.appxupload
228 |
229 | # Visual Studio cache files
230 | # files ending in .cache can be ignored
231 | *.[Cc]ache
232 | # but keep track of directories ending in .cache
233 | !?*.[Cc]ache/
234 |
235 | # Others
236 | ClientBin/
237 | ~$*
238 | *~
239 | *.dbmdl
240 | *.dbproj.schemaview
241 | *.jfm
242 | *.pfx
243 | *.publishsettings
244 | orleans.codegen.cs
245 |
246 | # Including strong name files can present a security risk
247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
248 | #*.snk
249 |
250 | # Since there are multiple workflows, uncomment next line to ignore bower_components
251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
252 | #bower_components/
253 |
254 | # RIA/Silverlight projects
255 | Generated_Code/
256 |
257 | # Backup & report files from converting an old project file
258 | # to a newer Visual Studio version. Backup files are not needed,
259 | # because we have git ;-)
260 | _UpgradeReport_Files/
261 | Backup*/
262 | UpgradeLog*.XML
263 | UpgradeLog*.htm
264 | ServiceFabricBackup/
265 | *.rptproj.bak
266 |
267 | # SQL Server files
268 | *.mdf
269 | *.ldf
270 | *.ndf
271 |
272 | # Business Intelligence projects
273 | *.rdl.data
274 | *.bim.layout
275 | *.bim_*.settings
276 | *.rptproj.rsuser
277 | *- [Bb]ackup.rdl
278 | *- [Bb]ackup ([0-9]).rdl
279 | *- [Bb]ackup ([0-9][0-9]).rdl
280 |
281 | # Microsoft Fakes
282 | FakesAssemblies/
283 |
284 | # GhostDoc plugin setting file
285 | *.GhostDoc.xml
286 |
287 | # Node.js Tools for Visual Studio
288 | .ntvs_analysis.dat
289 | node_modules/
290 |
291 | # Visual Studio 6 build log
292 | *.plg
293 |
294 | # Visual Studio 6 workspace options file
295 | *.opt
296 |
297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
298 | *.vbw
299 |
300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
301 | *.vbp
302 |
303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
304 | *.dsw
305 | *.dsp
306 |
307 | # Visual Studio 6 technical files
308 | *.ncb
309 | *.aps
310 |
311 | # Visual Studio LightSwitch build output
312 | **/*.HTMLClient/GeneratedArtifacts
313 | **/*.DesktopClient/GeneratedArtifacts
314 | **/*.DesktopClient/ModelManifest.xml
315 | **/*.Server/GeneratedArtifacts
316 | **/*.Server/ModelManifest.xml
317 | _Pvt_Extensions
318 |
319 | # Paket dependency manager
320 | .paket/paket.exe
321 | paket-files/
322 |
323 | # FAKE - F# Make
324 | .fake/
325 |
326 | # CodeRush personal settings
327 | .cr/personal
328 |
329 | # Python Tools for Visual Studio (PTVS)
330 | __pycache__/
331 | *.pyc
332 |
333 | # Cake - Uncomment if you are using it
334 | # tools/**
335 | # !tools/packages.config
336 |
337 | # Tabs Studio
338 | *.tss
339 |
340 | # Telerik's JustMock configuration file
341 | *.jmconfig
342 |
343 | # BizTalk build output
344 | *.btp.cs
345 | *.btm.cs
346 | *.odx.cs
347 | *.xsd.cs
348 |
349 | # OpenCover UI analysis results
350 | OpenCover/
351 |
352 | # Azure Stream Analytics local run output
353 | ASALocalRun/
354 |
355 | # MSBuild Binary and Structured Log
356 | *.binlog
357 |
358 | # NVidia Nsight GPU debugger configuration file
359 | *.nvuser
360 |
361 | # MFractors (Xamarin productivity tool) working folder
362 | .mfractor/
363 |
364 | # Local History for Visual Studio
365 | .localhistory/
366 |
367 | # Visual Studio History (VSHistory) files
368 | .vshistory/
369 |
370 | # BeatPulse healthcheck temp database
371 | healthchecksdb
372 |
373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
374 | MigrationBackup/
375 |
376 | # Ionide (cross platform F# VS Code tools) working folder
377 | .ionide/
378 |
379 | # Fody - auto-generated XML schema
380 | FodyWeavers.xsd
381 |
382 | # VS Code files for those working on multiple tools
383 | .vscode/*
384 | !.vscode/settings.json
385 | !.vscode/tasks.json
386 | !.vscode/launch.json
387 | !.vscode/extensions.json
388 | *.code-workspace
389 |
390 | # Local History for Visual Studio Code
391 | .history/
392 |
393 | # Windows Installer files from build outputs
394 | *.cab
395 | *.msi
396 | *.msix
397 | *.msm
398 | *.msp
399 |
400 | # JetBrains Rider
401 | *.sln.iml
402 |
403 | ##
404 | ## Visual studio for Mac
405 | ##
406 |
407 |
408 | # globs
409 | Makefile.in
410 | *.userprefs
411 | *.usertasks
412 | config.make
413 | config.status
414 | aclocal.m4
415 | install-sh
416 | autom4te.cache/
417 | *.tar.gz
418 | tarballs/
419 | test-results/
420 |
421 | # Mac bundle stuff
422 | *.dmg
423 | *.app
424 |
425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
426 | # General
427 | .DS_Store
428 | .AppleDouble
429 | .LSOverride
430 |
431 | # Icon must end with two \r
432 | Icon
433 |
434 |
435 | # Thumbnails
436 | ._*
437 |
438 | # Files that might appear in the root of a volume
439 | .DocumentRevisions-V100
440 | .fseventsd
441 | .Spotlight-V100
442 | .TemporaryItems
443 | .Trashes
444 | .VolumeIcon.icns
445 | .com.apple.timemachine.donotpresent
446 |
447 | # Directories potentially created on remote AFP share
448 | .AppleDB
449 | .AppleDesktop
450 | Network Trash Folder
451 | Temporary Items
452 | .apdisk
453 |
454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
455 | # Windows thumbnail cache files
456 | Thumbs.db
457 | ehthumbs.db
458 | ehthumbs_vista.db
459 |
460 | # Dump file
461 | *.stackdump
462 |
463 | # Folder config file
464 | [Dd]esktop.ini
465 |
466 | # Recycle Bin used on file shares
467 | $RECYCLE.BIN/
468 |
469 | # Windows Installer files
470 | *.cab
471 | *.msi
472 | *.msix
473 | *.msm
474 | *.msp
475 |
476 | # Windows shortcuts
477 | *.lnk
478 |
479 | .mono
480 |
481 | conf.json
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using Program.Comm;
2 | using System.Net.WebSockets;
3 |
4 | namespace Program {
5 | public class MainProgram {
6 | static readonly Cmd cmd = new();
7 | static readonly Config cfg = new();
8 |
9 | static readonly List clients = new();
10 | static readonly List servers = new();
11 |
12 | static bool exit = false;
13 |
14 | static int curIndex = -1;
15 | static bool isServer = false;
16 |
17 | private static void PrintTopMenu() {
18 | Console.WriteLine("Commands");
19 | Console.WriteLine("\tls - List all server connections.");
20 | Console.WriteLine("\tlc - List all client connections.");
21 | Console.WriteLine("\tnew - Establish a client connection with :.");
22 | Console.WriteLine("\tcc - Use client at index .");
23 | Console.WriteLine("\tcs - Use server at index .");
24 | Console.WriteLine("\trc - Remove client at index .");
25 | Console.WriteLine("\trs - Remove server at index .");
26 | Console.WriteLine("\th - Print top/help menu.");
27 | Console.WriteLine("\tq - Exit program.");
28 | }
29 |
30 | /* Listing */
31 | private static void ListClients() {
32 | for (int i = 0; i < clients.Count; i++) {
33 | var cl = clients[i];
34 |
35 | Console.WriteLine($"[{i}] {cl.Server.host}:{cl.Server.port} (SSL => {cl.Ssl})");
36 | }
37 | }
38 |
39 | private static void ListServers() {
40 | for (int i = 0; i < servers.Count; i++) {
41 | var srv = servers[i];
42 |
43 | Console.WriteLine($"[{i}] {srv.Bind.host ?? "N/A"}:{srv.Bind.port} (SSL => {srv.Ssl})");
44 | }
45 | }
46 |
47 | /* Retrieving indexes */
48 | private static int GetClientIndex(Client cl) {
49 | return clients.FindIndex(c => cl == c);
50 | }
51 |
52 | private static int GetServerIndex(Server srv) {
53 | return servers.FindIndex(c => c == srv);
54 | }
55 |
56 | /* Message processing */
57 | private static void ProcessClientMsg(Client cl, string msg) {
58 | var idx = GetClientIndex(cl);
59 |
60 | if (curIndex == idx && !isServer && msg != "")
61 | Console.Write($"\nServer: {msg}\nMsg: ");
62 | }
63 |
64 | private static void ProcessServerMsg(Server srv, string msg) {
65 | var idx = GetServerIndex(srv);
66 |
67 | if (curIndex == idx && isServer)
68 | Console.Write($"\nClient: {msg}\nMsg: ");
69 | }
70 |
71 | /* General Processing */
72 | private static async void ClientProcess(Client cl) {
73 | while (true) {
74 | try {
75 | if (cl.Ws.State != WebSocketState.Open) {
76 | Console.WriteLine($"Found client connection to '{cl.Server.host}:{cl.Server.port}' closed. Aborting processing...");
77 |
78 | break;
79 | }
80 |
81 | var msg = await cl.Recv();
82 |
83 | ProcessClientMsg(cl, msg);
84 | } catch (Exception e) {
85 | var idx = GetClientIndex(cl);
86 |
87 | // Only print exception if we're active.
88 | if (curIndex == idx && !isServer)
89 | Console.WriteLine($"Failed to receive message from server due to exception. Exception:\n{e}");
90 |
91 | Thread.Sleep(1000);
92 | }
93 | }
94 | }
95 |
96 | private static async Task ServerProcessClient(Server srv, WebSocket ws) {
97 | while (true) {
98 | try {
99 | if (ws.State == WebSocketState.Open) {
100 | var msg = await srv.Recv();
101 |
102 | // If null, indicates an issue or close. So break and reallow new clients.
103 | if (msg == null)
104 | break;
105 |
106 | // Process message.
107 | ProcessServerMsg(srv, msg);
108 | } else {
109 | Console.WriteLine($"Found connection to server '{srv.Bind.host}:{srv.Bind.port}' that isn't open. Closing current connection.");
110 |
111 | break;
112 | }
113 | } catch (Exception e) {
114 | Console.WriteLine($"Found exception when receiving reply from client on server '{srv.Bind.host}:{srv.Bind.port}'. Closing current connection.");
115 | Console.WriteLine(e);
116 |
117 | break;
118 | }
119 | }
120 | }
121 |
122 | private static async Task ServerProcess(Server srv) {
123 | while (true) {
124 | var ctx = await srv.Listener.GetContextAsync();
125 |
126 | if (ctx.Request.IsWebSocketRequest) {
127 | var wsCtx = await ctx.AcceptWebSocketAsync(subProtocol: null);
128 | srv.Ws = wsCtx.WebSocket;
129 |
130 | await ServerProcessClient(srv, srv.Ws);
131 |
132 | // Attempt to close current web socket since we're done.
133 | try {
134 | await srv.Disconnect();
135 | } catch {}
136 |
137 | srv.Ws = null;
138 |
139 | continue;
140 | } else {
141 | ctx.Response.StatusCode = 500;
142 | ctx.Response.Close();
143 | }
144 | }
145 | }
146 |
147 | private static async Task RemoveClient(int idx) {
148 | try {
149 | // Retrieve client at index.
150 | var cl = clients[idx];
151 |
152 | try {
153 | await cl.Disconnect();
154 | } catch (Exception e) {
155 | Console.WriteLine($"Failed to disconnect client at index {idx} due to exception.");
156 | Console.WriteLine(e);
157 | }
158 |
159 | if (cl.Task != null) {
160 | try {
161 | cl.Task.Dispose();
162 | } catch (Exception e) {
163 | Console.WriteLine("Failed to stop task when disconnecting client at index {idx} due to exception.");
164 | Console.WriteLine(e);
165 | }
166 | }
167 |
168 | Console.WriteLine($"Removing client connection to '{cl.Server.host}:{cl.Server.port}'...");
169 |
170 | // Remove from clients list.
171 | clients.RemoveAt(idx);
172 | } catch (Exception e) {
173 | throw new Exception($"Failed to remove client at index #{idx} due to exception. Exception:\n{e}");
174 | }
175 | }
176 |
177 | private static async Task RemoveServer(int idx) {
178 | try {
179 | // Retrieve server.
180 | var srv = servers[idx];
181 |
182 | // Attempt to disconnect connection.
183 | try {
184 | await srv.Disconnect();
185 | } catch (Exception e) {
186 | Console.WriteLine($"Failed to disconnect server at index {idx} due to exception.");
187 | Console.WriteLine(e);
188 | }
189 |
190 | // Remove server from list.
191 | servers.RemoveAt(idx);
192 | } catch (Exception e) {
193 | Console.WriteLine($"Failed to remove server at index {idx} due to exception. Exception:\n{e}");
194 | }
195 | }
196 |
197 | private static async Task MakeConnection(string host, ushort port, bool ssl = true) {
198 | // Make sure we have a valid IP.
199 | if (!Utils.IsValidIpv4(host))
200 | throw new Exception($"Failed to make connection using '{host}:{port}' due to invalid host address. SSL => {ssl}.");
201 |
202 | var cl = new Client() {
203 | Server = new() {
204 | host = host,
205 | port = port
206 | },
207 | Ssl = ssl
208 | };
209 |
210 | // Attempt to connect to server.
211 | try {
212 | await cl.Connect();
213 | } catch (Exception e) {
214 | throw new Exception($"Failed to make connection using '{host}:{port}' due to connection error. SSL => {ssl}. Exception:\n{e}");
215 | }
216 |
217 | // Add clients to list.
218 | clients.Add(cl);
219 | }
220 |
221 | private static async Task ParseTopLine(string line) {
222 | // Get first argument.
223 | var split = line.Split(" ");
224 |
225 | switch (split[0]) {
226 | case "ls":
227 | Console.WriteLine("Listing Servers...");
228 |
229 | ListServers();
230 |
231 | break;
232 |
233 | case "lc":
234 | Console.WriteLine("Listing Clients...");
235 |
236 | ListClients();
237 |
238 | break;
239 |
240 | case "new": {
241 | if (split.Length < 2) {
242 | Console.WriteLine("IP not set.");
243 |
244 | break;
245 | }
246 |
247 | if (split.Length < 3) {
248 | Console.WriteLine("Port not set.");
249 |
250 | break;
251 | }
252 |
253 | var ip ="";
254 | var port = "";
255 | var ssl = true;
256 |
257 | try {
258 | ip = split[1];
259 | port = split[2];
260 |
261 | if (split.Length > 3) {
262 | var sslStr = split[3];
263 |
264 | if (sslStr.ToLower() == "no")
265 | ssl = false;
266 | }
267 | } catch (Exception e) {
268 | Console.WriteLine("Bad arguments due to exception.");
269 | Console.WriteLine(e);
270 | }
271 |
272 | try {
273 | await MakeConnection(ip, Convert.ToUInt16(port), ssl);
274 | } catch (Exception e) {
275 | Console.WriteLine($"Failed to make connection to '{ip ?? "N/A"}:{port}' due to exception. Exception:\n{e}");
276 | }
277 |
278 | break;
279 | }
280 |
281 | case "cc": {
282 | if (split.Length < 2) {
283 | Console.WriteLine("No index set.");
284 |
285 | break;
286 | }
287 |
288 | var idx = "";
289 |
290 | try {
291 | idx = split[1];
292 |
293 | curIndex = Convert.ToInt16(idx);
294 | isServer = false;
295 |
296 | Console.WriteLine($"Connecting to client at index {curIndex}...");
297 | } catch (Exception e) {
298 | Console.WriteLine($"Failed to switch to client {idx} due to exception. Exception:\n{e}");
299 | }
300 |
301 | break;
302 | }
303 |
304 | case "cs": {
305 | if (split.Length < 2) {
306 | Console.WriteLine("No index set.");
307 |
308 | break;
309 | }
310 |
311 | var idx = "";
312 |
313 | try {
314 | idx = split[1];
315 |
316 | curIndex = Convert.ToInt16(idx);
317 | isServer = true;
318 |
319 | Console.WriteLine($"Connecting to server at index {curIndex}...");
320 | } catch (Exception e) {
321 | Console.WriteLine($"Failed to switch to server {idx} due to exception. Exception:\n{e}");
322 | }
323 |
324 | break;
325 | }
326 |
327 | case "rc": {
328 | if (split.Length < 2) {
329 | Console.WriteLine("No index set.");
330 |
331 | break;
332 | }
333 |
334 | var idx = "";
335 |
336 | try {
337 | idx = split[1];
338 |
339 | await RemoveClient(Convert.ToInt16(idx));
340 | } catch (Exception e) {
341 | Console.WriteLine($"Failed to remove client at index {idx} due to exception. Exception:\n{e}");
342 | }
343 |
344 | break;
345 | }
346 |
347 | case "rs": {
348 | if (split.Length < 2) {
349 | Console.WriteLine("No index set.");
350 |
351 | break;
352 | }
353 |
354 | var idx = "";
355 |
356 | try {
357 | idx = split[1];
358 |
359 | await RemoveServer(Convert.ToInt16(idx));
360 | } catch (Exception e) {
361 | Console.WriteLine($"Failed to remove server at index {idx} due to exception. Exception:\n{e}");
362 | }
363 |
364 | break;
365 | }
366 |
367 | case "h":
368 | PrintTopMenu();
369 |
370 | break;
371 |
372 | case "q":
373 | exit = true;
374 |
375 | break;
376 |
377 | default:
378 | PrintTopMenu();
379 |
380 | break;
381 | }
382 | }
383 |
384 | private static async Task HandleMessage(string msg) {
385 | // If we're receiving a quit message, reset.
386 | if (msg == "\\q") {
387 | curIndex = -1;
388 |
389 | return;
390 | }
391 |
392 | try {
393 | if (isServer) {
394 | // Attempt to retrieve current server.
395 | var srv = servers[curIndex];
396 |
397 | // Send the message to the client.
398 | try {
399 | await srv.Send(msg);
400 | } catch (Exception e) {
401 | throw new Exception($"Failed to send message to client due to exception. Exception:\n{e}");
402 | }
403 | } else {
404 | // Attempt to retrieve current client.
405 | var cl = clients[curIndex];
406 |
407 | // Attempt to send message to server.
408 | try {
409 | await cl.Send(msg);
410 | } catch (Exception e) {
411 | throw new Exception($"Failed to send message to server due to exception. Exception:\n{e}");
412 | }
413 | }
414 | } catch (Exception e) {
415 | var oldConn = curIndex;
416 |
417 | curIndex = -1;
418 |
419 | throw new Exception($"Failed to handle message for current connection #{oldConn} due to exception. Is server => {isServer}. Exception:\n{e}");
420 | }
421 | }
422 |
423 | private static void HandleAllIncoming() {
424 | while (true) {
425 | foreach (var cl in clients) {
426 | if (cl.Task != null)
427 | continue;
428 |
429 | try {
430 | // Create a cancellation token.
431 | var tokenSource2 = new CancellationTokenSource();
432 | CancellationToken ct = tokenSource2.Token;
433 |
434 | cl.Task = Task.Factory.StartNew(() => ClientProcess(cl));
435 | } catch (Exception e) {
436 | Console.WriteLine($"Failed to process client '{cl.Server.host}:{cl.Server.port}' due to exception.");
437 | Console.WriteLine(e);
438 | }
439 | }
440 |
441 | Thread.Sleep(1000);
442 | }
443 | }
444 |
445 | private static async Task StartupConnections() {
446 | foreach (var conn in cfg.StartupConnections) {
447 | try {
448 | await MakeConnection(conn.srv.host, conn.srv.port, conn.ssl);
449 | } catch (Exception e) {
450 | Console.WriteLine($"Failed to start up connection '{conn.srv.host}:{conn.srv.port}' (SSL => {conn.ssl}) due to exception.");
451 | Console.WriteLine(e);
452 | }
453 | }
454 | }
455 |
456 | private static async Task HandleListenServer() {
457 | var ssl = cfg.ListenSsl;
458 | var host = cfg.ListenHost;
459 | var port = cfg.ListenPort;
460 |
461 | // Check for command line overrides.
462 | if (cmd.Opts.Ssl.HasValue)
463 | ssl = cmd.Opts.Ssl.Value;
464 |
465 | if (cmd.Opts.Host != null)
466 | host = cmd.Opts.Host;
467 |
468 | if (cmd.Opts.Port.HasValue)
469 | port = (ushort) cmd.Opts.Port.Value;
470 |
471 | servers.Add(new() {
472 | Ssl = ssl,
473 | Bind = new() {
474 | host = host,
475 | port = port
476 | }
477 | });
478 |
479 | var srv = servers[^1];
480 |
481 | // Attempt to listen.
482 | try {
483 | srv.Listen();
484 | } catch (Exception e) {
485 | Console.WriteLine($"Failed to listen on '{host}:{port}' due to exception.");
486 | Console.WriteLine(e);
487 |
488 | return;
489 | }
490 |
491 | // Attempt to process server messages.
492 | try {
493 | await ServerProcess(srv);
494 | } catch (Exception e) {
495 | Console.WriteLine($"Failed to proces server '{host}:{port}' due to exception.");
496 | Console.WriteLine(e);
497 | }
498 | }
499 |
500 | static async Task Main(string[] args) {
501 | // Parse command line options.
502 | try {
503 | cmd.Parse(args);
504 | } catch (Exception e) {
505 | Console.WriteLine("Failed to parse command line due to exception.");
506 | Console.WriteLine(e);
507 |
508 | return 1;
509 | }
510 |
511 | // Parse config.
512 | try {
513 | if (cmd.Opts.Cfg == null)
514 | Console.WriteLine("Config path somehow null?");
515 | else
516 | cfg.Load(cmd.Opts.Cfg);
517 | } catch (Exception e) {
518 | Console.WriteLine("Failed to load and read config file due to exception.");
519 | Console.WriteLine(e);
520 | }
521 |
522 | // Check if we should print config and exit.
523 | if (cmd.Opts.List) {
524 | cfg.Print();
525 |
526 | return 0;
527 | }
528 |
529 | // Connect to startup servers from config.
530 | try {
531 | await StartupConnections();
532 | } catch (Exception e) {
533 | Console.WriteLine("Failed to start initial server connections due to exception.");
534 | Console.WriteLine(e);
535 | }
536 |
537 | // We'll want to spin up a new task to handle adding client connections.
538 | #pragma warning disable CS4014
539 | Task.Factory.StartNew(() => HandleAllIncoming());
540 | #pragma warning restore CS4014
541 |
542 | // Spin up another task for listen server if enabled.
543 | var listen = cfg.Listen;
544 |
545 | if (cmd.Opts.NoListen)
546 | listen = false;
547 |
548 | if (listen) {
549 | Console.WriteLine($"Attempting to listen on '{cfg.ListenHost}:{cfg.ListenPort}'...");
550 |
551 | #pragma warning disable CS4014
552 | Task.Factory.StartNew(() => HandleListenServer());
553 | #pragma warning restore CS4014
554 | }
555 |
556 | // Print top menu now.
557 | PrintTopMenu();
558 |
559 | while (!exit) {
560 | // Check our current connection.
561 | if (curIndex == -1) {
562 | Console.Write("Cmd: ");
563 | try {
564 | var input = Console.ReadLine();
565 |
566 | if (input != null)
567 | await ParseTopLine(input);
568 |
569 | } catch (Exception e) {
570 | Console.WriteLine("Failed to read user input due to exception.");
571 | Console.WriteLine(e);
572 |
573 | return 1;
574 | }
575 | } else {
576 | try {
577 | // Note to self; PLEASE IMPROVE THE BELOW IN THE FUTURE. IT'S BAD!
578 | if (isServer) {
579 | var srv = servers[curIndex];
580 | }
581 | else {
582 | var cl = clients[curIndex];
583 | }
584 | } catch (Exception e) {
585 | Console.WriteLine($"Failed to connect to {(isServer ? "server" : "client")} at index {curIndex}");
586 | Console.WriteLine(e);
587 |
588 | curIndex = -1;
589 |
590 | continue;
591 | }
592 |
593 | Console.Write("Msg: ");
594 |
595 | try {
596 | var input = Console.ReadLine();
597 |
598 | if (input != null)
599 | await HandleMessage(input);
600 | } catch (Exception e) {
601 | Console.WriteLine($"Failed to handle message due to exception.");
602 | Console.WriteLine(e);
603 |
604 | continue;
605 | }
606 | }
607 | }
608 |
609 | return 0;
610 | }
611 | }
612 | }
--------------------------------------------------------------------------------