|
2 | 2 | using System;
|
3 | 3 | using System.Collections.Generic;
|
4 | 4 | using System.Linq;
|
| 5 | +using System.Net; |
| 6 | +using System.Net.Sockets; |
5 | 7 | using System.Text;
|
6 | 8 | using System.Threading.Tasks;
|
7 | 9 |
|
8 | 10 |
|
9 | 11 | namespace Beef.BeefApi {
|
| 12 | + /// <summary> |
| 13 | + /// The ApiServer hosts a REST endpoint at /beef-ladder that returns a json representation |
| 14 | + /// of the current ladder rankings. It also provides a socket that can be connected to at |
| 15 | + /// the configured port in order to be notified realtime when the ladder has been changed. |
| 16 | + /// </summary> |
10 | 17 | public class ApiServer {
|
11 | 18 | private String _configFilePath;
|
12 | 19 | private SynchronizationContext _mainContext;
|
13 | 20 | private BeefUserConfigManager _beefUserManager;
|
14 | 21 | private PresentationManager _ladderManager;
|
15 | 22 | private WebApplication _application;
|
16 | 23 | private Thread _thread;
|
| 24 | + private SynchronizationContext _threadContext; |
| 25 | + private int _eventSocket; // set from the config file |
| 26 | + private List<Socket> _eventClients = new List<Socket>(); |
17 | 27 | private object _lock = new object();
|
18 | 28 |
|
19 | 29 | public ApiServer(String configFilePath, SynchronizationContext mainContext, PresentationManager ladderManager, BeefUserConfigManager beefUserManager) {
|
20 | 30 | _configFilePath = configFilePath;
|
21 | 31 | _mainContext = mainContext;
|
22 | 32 | _ladderManager = ladderManager;
|
23 | 33 | _beefUserManager = beefUserManager;
|
| 34 | + |
| 35 | + // Subscribe to the ladder changed event |
| 36 | + _ladderManager.LadderChanged += OnLadderChanged; |
24 | 37 | }
|
25 | 38 |
|
26 | 39 | public void Start() {
|
@@ -51,9 +64,13 @@ public void Stop() {
|
51 | 64 | }
|
52 | 65 |
|
53 | 66 | private void ThreadStart() {
|
| 67 | + SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); |
| 68 | + _threadContext = SynchronizationContext.Current; |
| 69 | + |
54 | 70 | lock (_lock) {
|
55 | 71 | var builder = WebApplication.CreateBuilder();
|
56 | 72 | builder.Configuration.AddJsonFile(_configFilePath);
|
| 73 | + _eventSocket = builder.Configuration.GetValue<int>("LadderChangedEventPort", 5002); |
57 | 74 |
|
58 | 75 | _application = builder.Build();
|
59 | 76 | Monitor.PulseAll(_lock);
|
@@ -117,7 +134,85 @@ await Task.Run(() => {
|
117 | 134 | await context.Response.WriteAsJsonAsync(response);
|
118 | 135 | });
|
119 | 136 |
|
| 137 | + _threadContext.Post(async (object? state) => { |
| 138 | + IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, _eventSocket); |
| 139 | + Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); |
| 140 | + listener.Bind(localEndPoint); |
| 141 | + listener.Listen(); |
| 142 | + |
| 143 | + while (true) { |
| 144 | + Socket client = await listener.AcceptAsync(); |
| 145 | + |
| 146 | + lock (_eventClients) { |
| 147 | + _eventClients.Add(client); |
| 148 | + } |
| 149 | + } |
| 150 | + }, null); |
| 151 | + |
120 | 152 | _application.Run();
|
121 | 153 | }
|
| 154 | + |
| 155 | + private void NotifyLadderChanged() { |
| 156 | + List<Socket> clientsToRemove = new List<Socket>(); |
| 157 | + lock (_eventClients) { |
| 158 | + foreach (Socket client in _eventClients) { |
| 159 | + if (!client.Connected) { |
| 160 | + clientsToRemove.Add(client); |
| 161 | + continue; |
| 162 | + } |
| 163 | + |
| 164 | + try { |
| 165 | + String eventMessage = "{ \"Message\": \"OnLadderChanged\" }"; |
| 166 | + int messageLength = eventMessage.Length; |
| 167 | + byte[] lengthBytes = GetBytesInNetworkOrder(messageLength); |
| 168 | + byte[] messageBytes = Encoding.UTF8.GetBytes(eventMessage); |
| 169 | + |
| 170 | + SendBytesOrDie(lengthBytes, client); |
| 171 | + SendBytesOrDie(messageBytes, client); |
| 172 | + } catch (Exception) { |
| 173 | + // Any exception should just close the connection and keep going |
| 174 | + clientsToRemove.Add(client); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + // Cleanup anyone that has disconnected |
| 179 | + foreach (Socket client in clientsToRemove) { |
| 180 | + _eventClients.Remove(client); |
| 181 | + } |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + private byte[] GetBytesInNetworkOrder(int number) { |
| 186 | + byte[] numberBytes = BitConverter.GetBytes(number); |
| 187 | + if (BitConverter.IsLittleEndian) |
| 188 | + Array.Reverse(numberBytes); |
| 189 | + return numberBytes; |
| 190 | + } |
| 191 | + |
| 192 | + private void OnLadderChanged(List<BeefEntry> entries) { |
| 193 | + _threadContext.Post((object? state) => { |
| 194 | + NotifyLadderChanged(); |
| 195 | + }, null); |
| 196 | + } |
| 197 | + |
| 198 | + /// <summary> |
| 199 | + /// Sends the given bytes to the given socket. If all the bytes aren't sent, an |
| 200 | + /// exception is thrown. Note that in the event the buffer was sent in stages and |
| 201 | + /// an error occurs inbetween the stages, some bytes could have been sent prior to the exception. |
| 202 | + /// </summary> |
| 203 | + /// <param name="bytes">The bytes to send to the socket.</param> |
| 204 | + /// <param name="socket">The socket to write to. It is assumed it's already connected.</param> |
| 205 | + private void SendBytesOrDie(byte[] bytes, Socket socket) { |
| 206 | + int bytesSent = 0; |
| 207 | + int index = 0; |
| 208 | + while (bytesSent < bytes.Length) { |
| 209 | + int result = socket.Send(bytes, index, bytes.Length - index, SocketFlags.None); |
| 210 | + if (result <= 0) { |
| 211 | + throw new SocketException(result); |
| 212 | + } else { |
| 213 | + bytesSent += result; |
| 214 | + } |
| 215 | + } |
| 216 | + } |
122 | 217 | }
|
123 | 218 | }
|
0 commit comments