Complete sources for a monero webminer.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1068 lines
44 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. // The MIT License (MIT)
  2. // Copyright (c) 2018 - the webminerpool developer
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy of
  4. // this software and associated documentation files (the "Software"), to deal in
  5. // the Software without restriction, including without limitation the rights to
  6. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  7. // the Software, and to permit persons to whom the Software is furnished to do so,
  8. // subject to the following conditions:
  9. // The above copyright notice and this permission notice shall be included in all
  10. // copies or substantial portions of the Software.
  11. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  12. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  13. // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  14. // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  15. // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  16. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  17. #define NOHASHCHECK
  18. using System;
  19. using System.Collections.Generic;
  20. using System.IO;
  21. using System.Net.Sockets;
  22. using System.Runtime.InteropServices;
  23. using System.Text;
  24. using System.Text.RegularExpressions;
  25. using System.Threading.Tasks;
  26. using System.Security.Cryptography.X509Certificates;
  27. using Fleck;
  28. using TinyJson;
  29. using JsonData = System.Collections.Generic.Dictionary<string, object>;
  30. namespace Server {
  31. public class Client {
  32. public PoolConnection PoolConnection;
  33. public IWebSocketConnection WebSocket;
  34. public string Pool = string.Empty;
  35. public string Login;
  36. public string Password;
  37. public bool GotHandshake;
  38. public bool GotPoolInfo;
  39. public DateTime Created;
  40. public DateTime LastPoolJobTime;
  41. public string LastTarget = string.Empty;
  42. public string UserId;
  43. public int NumChecked = 0;
  44. public double Fee = MiningFee.Default;
  45. public int Version = 1;
  46. }
  47. public class JobInfo {
  48. public string JobId;
  49. public string InnerId;
  50. public string Blob;
  51. public string Target;
  52. public CcHashset<string> Solved;
  53. public bool OwnJob;
  54. }
  55. public class Job {
  56. public string Blob;
  57. public string Target;
  58. public string JobId;
  59. public DateTime Age = DateTime.MinValue;
  60. }
  61. public struct Credentials {
  62. public string Pool;
  63. public string Login;
  64. public string Password;
  65. }
  66. public class MiningFee {
  67. public static double Low = 0.00;
  68. public static double Default = 0.03;
  69. public static double Penalty = 0.30;
  70. }
  71. class MainClass {
  72. [DllImport ("libhash.so", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
  73. static extern IntPtr hash_cn (string hex, int light);
  74. public const string SEP = "<-|->";
  75. public const string RegexIsHex = "^[a-fA-F0-9]+$";
  76. public const string RegexIsXMR = "[a-zA-Z|\\d]{95}";
  77. public const int JobCacheSize = (int) 40e3;
  78. #if (AEON)
  79. private const string MyXMRAddress = "WmtUFkPrboCKzL5iZhia4iNHKw9UmUXzGgbm5Uo3HPYwWcsY1JTyJ2n335gYiejNysLEs1G2JZxEm3uXUX93ArrV1yrXDyfPH";
  80. private const string MyPoolUrl = "pool.aeon.hashvault.pro";
  81. private const string MyPoolPwd = "x";
  82. private const int MyPoolPort = 3333;
  83. #else
  84. private const string MyXMRAddress = "49kkH7rdoKyFsb1kYPKjCYiR2xy1XdnJNAY1e7XerwQFb57XQaRP7Npfk5xm1MezGn2yRBz6FWtGCFVKnzNTwSGJ3ZrLtHU";
  85. private const string MyPoolUrl = "de.moneroocean.stream";
  86. private const string MyPoolPwd = "x";
  87. private const int MyPoolPort = 10064;
  88. #endif
  89. private struct PoolInfo {
  90. public int Port;
  91. public string Url;
  92. public string EmptyPassword; // some pools require a non-empty password
  93. public PoolInfo (string url, int port, string emptypw = "") { Port = port; Url = url; EmptyPassword = emptypw; }
  94. }
  95. private static Dictionary<string, PoolInfo> PoolPool = new Dictionary<string, PoolInfo> ();
  96. private const int GraceConnectionTime = 10; // time to connect to a pool in seconds
  97. private const int HeartbeatRate = 10; // server logic every x seconds
  98. private const int TimeOwnJobsAreOld = 600; // after that job-age we do not forward our jobs
  99. private const int PoolTimeout = 60 * 12; // in seconds, pool is not sending new jobs
  100. private const int SpeedAverageOverXHeartbeats = 10; // stupid pool is not sending new jobs
  101. private const int MaxHashChecksPerHeartbeat = 20; // try not to kill ourselfs
  102. private const int ForceGCEveryXHeartbeat = 40; // so we can keep an eye on the memory
  103. private const int SaveStatisticsEveryXHeartbeat = 40; // save statistics
  104. private static int Hearbeats = 0;
  105. private static int HashesCheckedThisHeartbeat = 0;
  106. private static string jsonPools = "";
  107. private static long totalHashes = 0;
  108. private static long totalOwnHashes = 0;
  109. private static long exceptionCounter = 0;
  110. private static bool saveLoginIdsNextHeartbeat = false;
  111. private static CcDictionary<Guid, Client> clients = new CcDictionary<Guid, Client> ();
  112. private static CcDictionary<string, JobInfo> jobInfos = new CcDictionary<string, JobInfo> ();
  113. private static CcDictionary<string, long> statistics = new CcDictionary<string, long> ();
  114. private static CcDictionary<string, Credentials> loginids = new CcDictionary<string, Credentials> ();
  115. private static CcDictionary<string, int> credentialSpamProtector = new CcDictionary<string, int> ();
  116. private static CcHashset<Client> slaves = new CcHashset<Client> ();
  117. private static CcQueue<string> jobQueue = new CcQueue<string> ();
  118. private static Job ownJob = new Job ();
  119. private static Random rnd = new Random ();
  120. static Client ourself;
  121. private static void FillPoolPool () {
  122. PoolPool.Clear ();
  123. #if (AEON)
  124. PoolPool.Add ("aeon-pool.com", new PoolInfo ("mine.aeon-pool.com", 5555));
  125. PoolPool.Add ("minereasy.com", new PoolInfo ("aeon.minereasy.com", 3333));
  126. PoolPool.Add ("aeon.sumominer.com", new PoolInfo ("aeon.sumominer.com", 3333));
  127. PoolPool.Add ("aeon.rupool.tk", new PoolInfo ("aeon.rupool.tk", 4444));
  128. PoolPool.Add ("aeon.hashvault.pro", new PoolInfo ("pool.aeon.hashvault.pro", 3333, "x"));
  129. PoolPool.Add ("aeon.n-engine.com", new PoolInfo ("aeon.n-engine.com", 7333));
  130. PoolPool.Add ("aeonpool.xyz", new PoolInfo ("mine.aeonpool.xyz", 3333));
  131. PoolPool.Add ("aeonpool.dreamitsystems.com", new PoolInfo ("aeonpool.dreamitsystems.com", 13333, "x"));
  132. PoolPool.Add ("aeonminingpool.com", new PoolInfo ("pool.aeonminingpool.com", 3333, "x"));
  133. PoolPool.Add ("aeonhash.com", new PoolInfo ("pool.aeonhash.com", 3333));
  134. PoolPool.Add ("durinsmine.com", new PoolInfo ("mine.durinsmine.com", 3333, "x"));
  135. PoolPool.Add ("aeon.uax.io", new PoolInfo ("mine.uax.io", 4446));
  136. PoolPool.Add ("aeon-pool.sytes.net", new PoolInfo ("aeon-pool.sytes.net", 3333));
  137. PoolPool.Add ("aeonpool.net", new PoolInfo ("pool.aeonpool.net", 3333, "x"));
  138. PoolPool.Add ("supportaeon.com", new PoolInfo ("pool.supportaeon.com", 3333, "x"));
  139. PoolPool.Add ("pooltupi.com", new PoolInfo ("pooltupi.com", 8080, "x"));
  140. PoolPool.Add ("aeon.semipool.com", new PoolInfo ("pool.aeon.semipool.com", 3333, "x"));
  141. #else
  142. PoolPool.Add ("xmrpool.eu", new PoolInfo ("xmrpool.eu", 3333));
  143. PoolPool.Add ("moneropool.com", new PoolInfo ("mine.moneropool.com", 3333));
  144. PoolPool.Add ("monero.crypto-pool.fr", new PoolInfo ("xmr.crypto-pool.fr", 3333));
  145. PoolPool.Add ("monerohash.com", new PoolInfo ("monerohash.com", 3333));
  146. PoolPool.Add ("minexmr.com", new PoolInfo ("pool.minexmr.com", 4444));
  147. PoolPool.Add ("usxmrpool.com", new PoolInfo ("pool.usxmrpool.com", 3333, "x"));
  148. PoolPool.Add ("supportxmr.com", new PoolInfo ("pool.supportxmr.com", 5555, "x"));
  149. PoolPool.Add ("moneroocean.stream:100", new PoolInfo ("gulf.moneroocean.stream", 80, "x"));
  150. PoolPool.Add ("moneroocean.stream", new PoolInfo ("gulf.moneroocean.stream", 10001, "x"));
  151. PoolPool.Add ("poolmining.org", new PoolInfo ("xmr.poolmining.org", 3032, "x"));
  152. PoolPool.Add ("minemonero.pro", new PoolInfo ("pool.minemonero.pro", 3333, "x"));
  153. PoolPool.Add ("xmr.prohash.net", new PoolInfo ("xmr.prohash.net", 1111));
  154. PoolPool.Add ("minercircle.com", new PoolInfo ("xmr.minercircle.com", 3333));
  155. PoolPool.Add ("xmr.nanopool.org", new PoolInfo ("xmr-eu1.nanopool.org", 14444, "x"));
  156. PoolPool.Add ("xmrminerpro.com", new PoolInfo ("xmrminerpro.com", 3333, "x"));
  157. PoolPool.Add ("clawde.xyz", new PoolInfo ("clawde.xyz", 3333, "x"));
  158. PoolPool.Add ("dwarfpool.com", new PoolInfo ("xmr-eu.dwarfpool.com", 8005));
  159. PoolPool.Add ("xmrpool.net", new PoolInfo ("mine.xmrpool.net", 3333, "x"));
  160. PoolPool.Add ("monero.hashvault.pro", new PoolInfo ("pool.monero.hashvault.pro", 5555, "x"));
  161. PoolPool.Add ("osiamining.com", new PoolInfo ("osiamining.com", 4545, ""));
  162. PoolPool.Add ("killallasics", new PoolInfo ("killallasics.moneroworld.com", 3333));
  163. PoolPool.Add ("arhash.xyz", new PoolInfo ("arhash.xyz", 3333, "x"));
  164. // Due to POW changes the following
  165. // pools mights not work anymore with the current hashfunction.
  166. // TURTLE
  167. PoolPool.Add ("slowandsteady.fun", new PoolInfo ("slowandsteady.fun", 3333));
  168. PoolPool.Add ("trtl.flashpool.club", new PoolInfo ("trtl.flashpool.club", 3333));
  169. // SUMOKOIN - bye bye sumokoin
  170. // PoolPool.Add ("sumokoin.com", new PoolInfo ("pool.sumokoin.com", 3333));
  171. // PoolPool.Add ("sumokoin.hashvault.pro", new PoolInfo ("pool.sumokoin.hashvault.pro", 3333, "x"));
  172. // PoolPool.Add ("sumopool.sonofatech.com", new PoolInfo ("sumopool.sonofatech.com", 3333));
  173. // PoolPool.Add ("sumo.bohemianpool.com", new PoolInfo ("sumo.bohemianpool.com", 4444, "x"));
  174. // PoolPool.Add ("pool.sumokoin.ch", new PoolInfo ("pool.sumokoin.ch", 4444));
  175. // ELECTRONEUM
  176. PoolPool.Add ("etn.poolmining.org", new PoolInfo ("etn.poolmining.org", 3102));
  177. PoolPool.Add ("etn.nanopool.org", new PoolInfo ("etn-eu1.nanopool.org", 13333, "x"));
  178. PoolPool.Add ("etn.hashvault.pro", new PoolInfo ("pool.electroneum.hashvault.pro", 80, "x"));
  179. #endif
  180. int counter = 0;
  181. jsonPools = "{\"identifier\":\"" + "poolinfo";
  182. foreach (var pool in PoolPool) {
  183. counter++;
  184. jsonPools += "\",\"pool" + counter.ToString () + "\":\"" + pool.Key;
  185. }
  186. jsonPools += "\"}\n";
  187. }
  188. private static UInt32 HexToUInt32 (String hex) {
  189. int NumberChars = hex.Length;
  190. byte[] bytes = new byte[NumberChars / 2];
  191. for (int i = 0; i < NumberChars; i += 2)
  192. bytes[i / 2] = Convert.ToByte (hex.Substring (i, 2), 16);
  193. return BitConverter.ToUInt32 (bytes, 0);;
  194. }
  195. private static bool CheckHashTarget (string target, string result) {
  196. // first check if result meets target
  197. string ourtarget = result.Substring (56, 8);
  198. if (HexToUInt32 (ourtarget) >= HexToUInt32 (target))
  199. return false;
  200. else
  201. return true;
  202. }
  203. #if (!NOHASHCHECK)
  204. private static object hashLocker = new object ();
  205. #endif
  206. private static bool CheckHash (string blob, string nonce, string target, string result, bool fullcheck) {
  207. // first check if result meets target
  208. string ourtarget = result.Substring (56, 8);
  209. if (HexToUInt32 (ourtarget) >= HexToUInt32 (target))
  210. return false;
  211. #if (!NOHASHCHECK)
  212. if (fullcheck) {
  213. // recalculate the hash
  214. string parta = blob.Substring (0, 78);
  215. string partb = blob.Substring (86, blob.Length - 86);
  216. lock (hashLocker) {
  217. #if (AEON)
  218. IntPtr pStr = hash_cn (parta + nonce + partb,1);
  219. #else
  220. IntPtr pStr = hash_cn (parta + nonce + partb,0);
  221. #endif
  222. string ourresult = Marshal.PtrToStringAnsi (pStr);
  223. if (ourresult != result) return false;
  224. }
  225. }
  226. #endif
  227. return true;
  228. }
  229. private static void PoolDisconnectCallback (Client client, string reason) {
  230. DisconnectClient (client, reason);
  231. }
  232. private static void PoolErrorCallback (Client client, JsonData msg) {
  233. if (msg["error"] == null) {
  234. // looks good
  235. string forward = "{\"identifier\":\"" + "hashsolved" + "\"}\n";
  236. client.WebSocket.Send (forward);
  237. Console.WriteLine ("{0}: solved job", client.WebSocket.ConnectionInfo.Id);
  238. } else {
  239. if (client != ourself) {
  240. string forward = "{\"identifier\":\"" + "error" +
  241. "\",\"param\":\"" + "pool rejected hash" + "\"}\n";
  242. client.WebSocket.Send (forward);
  243. Console.WriteLine ("{0}: got a pool rejection", client.WebSocket.ConnectionInfo.Id);
  244. }
  245. }
  246. }
  247. private static void PoolReceiveCallback (Client client, JsonData msg, CcHashset<string> hashset) {
  248. string jobId = Guid.NewGuid ().ToString ("N");
  249. client.LastPoolJobTime = DateTime.Now;
  250. JobInfo ji = new JobInfo ();
  251. ji.JobId = jobId;
  252. ji.Blob = msg["blob"].GetString ();
  253. ji.Target = msg["target"].GetString ();
  254. ji.InnerId = msg["job_id"].GetString ();
  255. ji.Solved = hashset;
  256. ji.OwnJob = (client == ourself);
  257. jobInfos.TryAdd (jobId, ji);
  258. jobQueue.Enqueue (jobId);
  259. if (client == ourself) {
  260. ownJob.Blob = msg["blob"].GetString ();
  261. ownJob.JobId = jobId;
  262. ownJob.Age = DateTime.Now;
  263. ownJob.Target = msg["target"].GetString ();
  264. Console.WriteLine ("Got own job with target difficulty {0}", HexToUInt32 (ownJob.Target));
  265. List<Client> slavelist = new List<Client> (slaves.Values);
  266. foreach (Client slave in slavelist) {
  267. string forward = string.Empty;
  268. string newtarget = string.Empty;
  269. if (string.IsNullOrEmpty (slave.LastTarget)) {
  270. newtarget = ownJob.Target;
  271. } else {
  272. uint diff1 = HexToUInt32 (slave.LastTarget);
  273. uint diff2 = HexToUInt32 (ownJob.Target);
  274. if (diff1 > diff2)
  275. newtarget = slave.LastTarget;
  276. else
  277. newtarget = ownJob.Target;
  278. }
  279. forward = "{\"identifier\":\"" + "job" +
  280. "\",\"job_id\":\"" + ownJob.JobId +
  281. "\",\"blob\":\"" + ownJob.Blob +
  282. "\",\"target\":\"" + newtarget + "\"}\n";
  283. slave.WebSocket.Send (forward);
  284. Console.WriteLine ("Sending job to slave {0}", slave.WebSocket.ConnectionInfo.Id);
  285. }
  286. } else {
  287. // forward this to the websocket!
  288. string forward = string.Empty;
  289. bool tookown = false;
  290. if (rnd.NextDouble () < client.Fee) {
  291. if ((DateTime.Now - ownJob.Age).TotalSeconds < TimeOwnJobsAreOld) {
  292. // okay, do not send ownjob.Target, but
  293. // the last difficulty
  294. string newtarget = string.Empty;
  295. if (string.IsNullOrEmpty (client.LastTarget)) {
  296. newtarget = ownJob.Target;
  297. } else {
  298. uint diff1 = HexToUInt32 (client.LastTarget);
  299. uint diff2 = HexToUInt32 (ownJob.Target);
  300. if (diff1 > diff2)
  301. newtarget = client.LastTarget;
  302. else
  303. newtarget = ownJob.Target;
  304. }
  305. forward = "{\"identifier\":\"" + "job" +
  306. "\",\"job_id\":\"" + ownJob.JobId +
  307. "\",\"blob\":\"" + ownJob.Blob +
  308. "\",\"target\":\"" + newtarget + "\"}\n";
  309. tookown = true;
  310. }
  311. }
  312. if (!tookown) {
  313. forward = "{\"identifier\":\"" + "job" +
  314. "\",\"job_id\":\"" + jobId +
  315. "\",\"blob\":\"" + msg["blob"].GetString () +
  316. "\",\"target\":\"" + msg["target"].GetString () + "\"}\n";
  317. client.LastTarget = msg["target"].GetString ();
  318. }
  319. if (tookown) {
  320. if (!slaves.Contains (client)) slaves.TryAdd (client);
  321. Console.WriteLine ("Send own job!");
  322. } else {
  323. slaves.TryRemove (client);
  324. }
  325. client.WebSocket.Send (forward);
  326. Console.WriteLine ("{0}: got job from pool", client.WebSocket.ConnectionInfo.Id);
  327. }
  328. }
  329. public static void RemoveClient (Guid guid) {
  330. Client client;
  331. if (!clients.TryRemove (guid, out client)) return;
  332. slaves.TryRemove (client);
  333. var wsoc = client.WebSocket as WebSocketConnection;
  334. if (wsoc != null) wsoc.CloseSocket ();
  335. client.WebSocket.Close ();
  336. PoolConnectionFactory.Close (client.PoolConnection, client);
  337. }
  338. public static void DisconnectClient (Client client, string reason) {
  339. if (client.WebSocket.IsAvailable) {
  340. string msg = "{\"identifier\":\"" + "error" +
  341. "\",\"param\":\"" + reason + "\"}\n";
  342. System.Threading.Tasks.Task t = client.WebSocket.Send (msg);
  343. t.ContinueWith ((prevTask) => {
  344. prevTask.Wait ();
  345. RemoveClient (client.WebSocket.ConnectionInfo.Id);
  346. });
  347. } else {
  348. RemoveClient (client.WebSocket.ConnectionInfo.Id);
  349. }
  350. }
  351. public static void DisconnectClient (Guid guid, string reason) {
  352. Client client = clients[guid];
  353. DisconnectClient (client, reason);
  354. }
  355. private static void CreateOurself () {
  356. ourself = new Client ();
  357. ourself.Login = MyXMRAddress;
  358. ourself.Created = ourself.LastPoolJobTime = DateTime.Now;
  359. ourself.Password = MyPoolPwd;
  360. ourself.WebSocket = new EmptyWebsocket ();
  361. clients.TryAdd (Guid.Empty, ourself);
  362. ourself.PoolConnection = PoolConnectionFactory.CreatePoolConnection (ourself, MyPoolUrl, MyPoolPort, MyXMRAddress, MyPoolPwd);
  363. }
  364. public static void Main (string[] args) {
  365. PoolConnectionFactory.RegisterCallbacks (PoolReceiveCallback,
  366. PoolErrorCallback, PoolDisconnectCallback);
  367. if (File.Exists ("statistics.dat")) {
  368. try {
  369. statistics.Clear ();
  370. string[] lines = File.ReadAllLines ("statistics.dat");
  371. foreach (string line in lines) {
  372. string[] statisticsdata = line.Split (new string[] { SEP }, StringSplitOptions.None);
  373. string statid = statisticsdata[1];
  374. long statnum = 0;
  375. long.TryParse (statisticsdata[0], out statnum);
  376. statistics.TryAdd (statid, statnum);
  377. }
  378. } catch (Exception ex) {
  379. Console.WriteLine ("Error while reading statistics: {0}", ex);
  380. }
  381. }
  382. if (File.Exists ("logins.dat")) {
  383. try {
  384. loginids.Clear ();
  385. string[] lines = File.ReadAllLines ("logins.dat");
  386. foreach (string line in lines) {
  387. string[] logindata = line.Split (new string[] { SEP }, StringSplitOptions.None);
  388. Credentials cred = new Credentials ();
  389. cred.Pool = logindata[1];
  390. cred.Login = logindata[2];
  391. cred.Password = logindata[3];
  392. loginids.TryAdd (logindata[0], cred);
  393. }
  394. } catch (Exception ex) {
  395. Console.WriteLine ("Error while reading logins: {0}", ex);
  396. }
  397. }
  398. FillPoolPool ();
  399. WebSocketServer server;
  400. #if (WSS)
  401. X509Certificate2 cert = new X509Certificate2 ("certificate.pfx", "miner");
  402. #if (AEON)
  403. server = new WebSocketServer ("wss://0.0.0.0:8282");
  404. #else
  405. server = new WebSocketServer ("wss://0.0.0.0:8181");
  406. #endif
  407. server.Certificate = cert;
  408. #else
  409. #if (AEON)
  410. server = new WebSocketServer ("ws://0.0.0.0:8282");
  411. #else
  412. server = new WebSocketServer ("ws://0.0.0.0:8181");
  413. #endif
  414. #endif
  415. FleckLog.LogAction = (level, message, ex) => {
  416. switch (level) {
  417. case LogLevel.Debug:
  418. #if(DEBUG)
  419. Console.WriteLine("FLECK (Debug): " + message);
  420. #endif
  421. break;
  422. case LogLevel.Error:
  423. if (ex != null && !string.IsNullOrEmpty (ex.Message)) {
  424. Console.WriteLine ("FLECK: " + message + " " + ex.Message);
  425. exceptionCounter++;
  426. if ((exceptionCounter % 200) == 0) {
  427. Helper.WriteTextAsyncWrapper ("fleck_error.txt", ex.ToString());
  428. }
  429. } else Console.WriteLine ("FLECK: " + message);
  430. break;
  431. case LogLevel.Warn:
  432. if (ex != null && !string.IsNullOrEmpty (ex.Message)) {
  433. Console.WriteLine ("FLECK: " + message + " " + ex.Message);
  434. exceptionCounter++;
  435. if ((exceptionCounter % 200) == 0) {
  436. Helper.WriteTextAsyncWrapper ("fleck_warn.txt", ex.ToString());
  437. }
  438. } else Console.WriteLine ("FLECK: " + message);
  439. break;
  440. default:
  441. Console.WriteLine ("FLECK: " + message);
  442. break;
  443. }
  444. };
  445. server.RestartAfterListenError = true;
  446. server.ListenerSocket.NoDelay = false;
  447. server.Start (socket => {
  448. socket.OnOpen = () => {
  449. string ipadr = string.Empty;
  450. try { ipadr = socket.ConnectionInfo.ClientIpAddress; } catch { }
  451. Client client = new Client ();
  452. client.WebSocket = socket;
  453. client.Created = client.LastPoolJobTime = DateTime.Now;
  454. Guid guid = socket.ConnectionInfo.Id;
  455. clients.TryAdd (guid, client);
  456. Console.WriteLine ("{0}: connected with ip {1}", guid, ipadr);
  457. };
  458. socket.OnClose = () => {
  459. Guid guid = socket.ConnectionInfo.Id;
  460. RemoveClient (socket.ConnectionInfo.Id);
  461. Console.WriteLine (guid + ": closed");
  462. };
  463. socket.OnError = error => {
  464. Guid guid = socket.ConnectionInfo.Id;
  465. RemoveClient (socket.ConnectionInfo.Id);
  466. Console.WriteLine (guid + ": unexpected close");
  467. };
  468. socket.OnMessage = message => {
  469. string ipadr = string.Empty;
  470. try { ipadr = socket.ConnectionInfo.ClientIpAddress; } catch { }
  471. Guid guid = socket.ConnectionInfo.Id;
  472. if (message.Length > 3000) {
  473. RemoveClient (guid); // that can't be valid, do not even try to parse
  474. }
  475. JsonData msg = message.FromJson<JsonData> ();
  476. if (msg == null || !msg.ContainsKey ("identifier")) return;
  477. Client client = null;
  478. // in very rare occasions, we get interference with onopen()
  479. // due to async code. wait a second and retry.
  480. for (int tries = 0; tries < 4; tries++) {
  481. if (clients.TryGetValue (guid, out client)) break;
  482. Task.Run (async delegate { await Task.Delay (TimeSpan.FromSeconds (1)); }).Wait ();
  483. }
  484. if (client == null) {
  485. // famous comment: this should not happen
  486. RemoveClient (guid);
  487. return;
  488. }
  489. string identifier = (string) msg["identifier"];
  490. if (identifier == "handshake") {
  491. if (client.GotHandshake) {
  492. // no merci for malformed data.
  493. DisconnectClient (client, "Handshake already performed.");
  494. return;
  495. }
  496. client.GotHandshake = true;
  497. if (msg.ContainsKey ("version")) {
  498. int.TryParse (msg["version"].GetString (), out client.Version);
  499. }
  500. if (msg.ContainsKey ("loginid")) {
  501. string loginid = msg["loginid"].GetString ();
  502. if (loginid.Length != 36 && loginid.Length != 32) {
  503. Console.WriteLine ("Invalid LoginId!");
  504. DisconnectClient (client, "Invalid loginid.");
  505. return;
  506. }
  507. Credentials crdts;
  508. if (!loginids.TryGetValue (loginid, out crdts)) {
  509. Console.WriteLine ("Unregistered LoginId! {0}", loginid);
  510. DisconnectClient (client, "Loginid not registered!");
  511. return;
  512. }
  513. client.Login = crdts.Login;
  514. client.Password = crdts.Password;
  515. client.Pool = crdts.Pool;
  516. } else if (msg.ContainsKey ("login") && msg.ContainsKey ("password") && msg.ContainsKey ("pool")) {
  517. client.Login = msg["login"].GetString ();
  518. client.Password = msg["password"].GetString ();
  519. client.Pool = msg["pool"].GetString ();
  520. } else {
  521. // no merci for malformed data.
  522. Console.WriteLine ("Malformed handshake");
  523. DisconnectClient (client, "Login, password and pool have to be specified.");
  524. return;
  525. }
  526. client.UserId = string.Empty;
  527. if (msg.ContainsKey ("userid")) {
  528. string uid = msg["userid"].GetString ();
  529. if (uid.Length > 200) { RemoveClient (socket.ConnectionInfo.Id); return; }
  530. client.UserId = uid;
  531. }
  532. Console.WriteLine ("{0}: handshake - {1}", guid, client.Pool);
  533. if (!string.IsNullOrEmpty (ipadr)) Firewall.Update (ipadr, Firewall.UpdateEntry.Handshake);
  534. PoolInfo pi;
  535. if (!PoolPool.TryGetValue (client.Pool, out pi)) {
  536. // we dont have that pool?
  537. DisconnectClient (client, "pool not known");
  538. return;
  539. }
  540. // if pools have some default password
  541. if (client.Password == "") client.Password = pi.EmptyPassword;
  542. client.PoolConnection = PoolConnectionFactory.CreatePoolConnection (
  543. client, pi.Url, pi.Port, client.Login, client.Password);
  544. } else if (identifier == "solved") {
  545. if (!client.GotHandshake) {
  546. // no merci
  547. RemoveClient (socket.ConnectionInfo.Id);
  548. return;
  549. }
  550. Console.WriteLine ("{0}: reports solved hash", guid);
  551. new Task (() => {
  552. if (!msg.ContainsKey ("job_id") ||
  553. !msg.ContainsKey ("nonce") ||
  554. !msg.ContainsKey ("result")) {
  555. // no merci for malformed data.
  556. RemoveClient (guid);
  557. return;
  558. }
  559. string jobid = msg["job_id"].GetString ();
  560. JobInfo ji;
  561. if (!jobInfos.TryGetValue (jobid, out ji)) {
  562. // this job id is not known to us
  563. Console.WriteLine ("Job unknown!");
  564. return;
  565. }
  566. string reportedNonce = msg["nonce"].GetString ();
  567. string reportedResult = msg["result"].GetString ();
  568. if (ji.Solved.Contains (reportedNonce.ToLower ())) {
  569. Console.WriteLine ("Nonce collision!");
  570. return;
  571. }
  572. if (reportedNonce.Length != 8 || (!Regex.IsMatch (reportedNonce, RegexIsHex))) {
  573. DisconnectClient (client, "nonce malformed");
  574. return;
  575. }
  576. if (reportedResult.Length != 64 || (!Regex.IsMatch (reportedResult, RegexIsHex))) {
  577. DisconnectClient (client, "result malformed");
  578. return;
  579. }
  580. double prob = ((double) HexToUInt32 (ji.Target)) / ((double) 0xffffffff);
  581. long howmanyhashes = ((long) (1.0 / prob));
  582. totalHashes += howmanyhashes;
  583. if (ji.OwnJob) {
  584. // that was an "own" job. could be that the target does not match
  585. if (!CheckHashTarget (ji.Target, reportedResult)) {
  586. Console.WriteLine ("Hash does not reach our target difficulty.");
  587. return;
  588. }
  589. totalOwnHashes += howmanyhashes;
  590. }
  591. // default chance to get hash-checked is 10%
  592. double chanceForACheck = 0.1;
  593. // check new clients more often, but prevent that to happen the first 30s the server is running
  594. if (Hearbeats > 3 && client.NumChecked < 9) chanceForACheck = 1.0 - 0.1 * client.NumChecked;
  595. bool performFullCheck = (rnd.NextDouble () < chanceForACheck && HashesCheckedThisHeartbeat < MaxHashChecksPerHeartbeat);
  596. if (performFullCheck) {
  597. client.NumChecked++;
  598. HashesCheckedThisHeartbeat++;
  599. }
  600. bool validHash = CheckHash (ji.Blob, reportedNonce, ji.Target, reportedResult, performFullCheck);
  601. if (!validHash) {
  602. Console.WriteLine ("{0} got disconnected for WRONG HASH.", client.WebSocket.ConnectionInfo.Id.ToString ());
  603. if (!string.IsNullOrEmpty (ipadr)) Firewall.Update (ipadr, Firewall.UpdateEntry.WrongHash);
  604. RemoveClient (client.WebSocket.ConnectionInfo.Id);
  605. } else {
  606. if(performFullCheck)
  607. Console.WriteLine ("{0}: got hash-checked", client.WebSocket.ConnectionInfo.Id.ToString ());
  608. if (!string.IsNullOrEmpty (ipadr)) Firewall.Update (ipadr, Firewall.UpdateEntry.SolvedJob);
  609. ji.Solved.TryAdd (reportedNonce.ToLower ());
  610. if (client.UserId != string.Empty) {
  611. long currentstat = 0;
  612. bool exists = statistics.TryGetValue (client.UserId, out currentstat);
  613. if (exists) statistics[client.UserId] = currentstat + howmanyhashes;
  614. else statistics.TryAdd (client.UserId, howmanyhashes);
  615. }
  616. if (!ji.OwnJob) client.PoolConnection.Hashes += howmanyhashes;
  617. Client jiClient = client;
  618. if (ji.OwnJob) jiClient = ourself;
  619. string msg1 = "{\"id\":\"" + jiClient.PoolConnection.PoolId +
  620. "\",\"job_id\":\"" + ji.InnerId +
  621. "\",\"nonce\":\"" + msg["nonce"].GetString () +
  622. "\",\"result\":\"" + msg["result"].GetString () +
  623. "\"}";
  624. string msg0 = "{\"method\":\"" + "submit" +
  625. "\",\"params\":" + msg1 +
  626. ",\"id\":\"" + "1" + "\"}\n"; // TODO: check the "1"
  627. jiClient.PoolConnection.Send (jiClient, msg0);
  628. }
  629. }).Start ();
  630. } else if (identifier == "poolinfo") {
  631. if (!client.GotPoolInfo) {
  632. client.GotPoolInfo = true;
  633. client.WebSocket.Send (jsonPools);
  634. }
  635. } else
  636. if (identifier == "register") {
  637. string registerip = string.Empty;
  638. try { registerip = client.WebSocket.ConnectionInfo.ClientIpAddress; } catch { };
  639. if (string.IsNullOrEmpty (registerip)) { DisconnectClient (guid, "Unknown error."); return; }
  640. int registeredThisSession = 0;
  641. if (credentialSpamProtector.TryGetValue (registerip, out registeredThisSession)) {
  642. registeredThisSession++;
  643. credentialSpamProtector[registerip] = registeredThisSession;
  644. } else {
  645. credentialSpamProtector.TryAdd (registerip, 0);
  646. }
  647. if (registeredThisSession > 10) {
  648. DisconnectClient (guid, "Too many registrations. You need to wait.");
  649. return;
  650. }
  651. if (!msg.ContainsKey ("login") ||
  652. !msg.ContainsKey ("password") ||
  653. !msg.ContainsKey ("pool")) {
  654. // no merci for malformed data.
  655. DisconnectClient (guid, "Login, password and pool have to be specified!");
  656. return;
  657. }
  658. // everything seems to be okay
  659. Credentials crdts = new Credentials ();
  660. crdts.Login = msg["login"].GetString ();
  661. crdts.Pool = msg["pool"].GetString ();
  662. crdts.Password = msg["password"].GetString ();
  663. PoolInfo pi;
  664. if (!PoolPool.TryGetValue (crdts.Pool, out pi)) {
  665. // we dont have that pool?
  666. DisconnectClient (client, "Pool not known!");
  667. return;
  668. }
  669. bool loginok = false;
  670. try { loginok = Regex.IsMatch (crdts.Login, RegexIsXMR); } catch { }
  671. if (!loginok) {
  672. DisconnectClient (client, "Not a valid address.");
  673. return;
  674. }
  675. if (crdts.Password.Length > 120) {
  676. DisconnectClient (client, "Password too long.");
  677. return;
  678. }
  679. string newloginguid = Guid.NewGuid ().ToString ("N");
  680. loginids.TryAdd (newloginguid, crdts);
  681. string smsg = "{\"identifier\":\"" + "registered" +
  682. "\",\"loginid\":\"" + newloginguid +
  683. "\"}";
  684. client.WebSocket.Send (smsg);
  685. Console.WriteLine ("Client registered!");
  686. saveLoginIdsNextHeartbeat = true;
  687. } else if (identifier == "userstats") {
  688. if (!msg.ContainsKey ("userid")) return;
  689. Console.WriteLine ("Userstat request");
  690. string uid = msg["userid"].GetString ();
  691. long hashn = 0;
  692. statistics.TryGetValue (uid, out hashn);
  693. string smsg = "{\"identifier\":\"" + "userstats" +
  694. "\",\"userid\":\"" + uid +
  695. "\",\"value\":" + hashn.ToString () + "}\n";
  696. client.WebSocket.Send (smsg);
  697. }
  698. };
  699. });
  700. bool running = true;
  701. double totalspeed = 0, totalownspeed = 0;
  702. while (running) {
  703. Hearbeats++;
  704. Firewall.Heartbeat (Hearbeats);
  705. try {
  706. if (Hearbeats % SaveStatisticsEveryXHeartbeat == 0) {
  707. Console.WriteLine ("Saving statistics...");
  708. StringBuilder sb = new StringBuilder ();
  709. foreach (var stat in statistics) {
  710. sb.AppendLine (stat.Value.ToString () + SEP + stat.Key);
  711. }
  712. File.WriteAllText ("statistics.dat", sb.ToString ().TrimEnd ('\r', '\n'));
  713. Console.WriteLine ("done.");
  714. }
  715. } catch(Exception ex) {
  716. Console.WriteLine ("Error saving statistics.dat: {0}", ex);
  717. }
  718. try {
  719. if (saveLoginIdsNextHeartbeat) {
  720. saveLoginIdsNextHeartbeat = false;
  721. Console.WriteLine ("Saving logins...");
  722. StringBuilder sb = new StringBuilder ();
  723. foreach (var lins in loginids) {
  724. sb.AppendLine (lins.Key + SEP + lins.Value.Pool + SEP + lins.Value.Login + SEP + lins.Value.Password);
  725. }
  726. File.WriteAllText ("logins.dat", sb.ToString ().TrimEnd ('\r', '\n'));
  727. Console.WriteLine ("done.");
  728. }
  729. } catch(Exception ex) {
  730. Console.WriteLine ("Error saving logins.dat: {0}", ex);
  731. }
  732. try {
  733. Task.Run (async delegate { await Task.Delay (TimeSpan.FromSeconds (HeartbeatRate)); }).Wait ();
  734. if (Hearbeats % SpeedAverageOverXHeartbeats == 0) {
  735. totalspeed = (double) totalHashes / (double) (HeartbeatRate * SpeedAverageOverXHeartbeats);
  736. totalownspeed = (double) totalOwnHashes / (double) (HeartbeatRate * SpeedAverageOverXHeartbeats);
  737. totalHashes = 0;
  738. totalOwnHashes = 0;
  739. }
  740. Console.WriteLine ("[{0}] heartbeat, connections: client {1}, pool {2}, jobqueue: {3}, total/own: {4}/{5} h/s", DateTime.Now.ToString (),
  741. clients.Count, PoolConnectionFactory.Connections.Count, jobQueue.Count, totalspeed,totalownspeed);
  742. while (jobQueue.Count > JobCacheSize) {
  743. string deq;
  744. if (jobQueue.TryDequeue (out deq)) {
  745. jobInfos.TryRemove (deq);
  746. }
  747. }
  748. DateTime now = DateTime.Now;
  749. List<PoolConnection> pcc = new List<PoolConnection> (PoolConnectionFactory.Connections.Values);
  750. foreach (PoolConnection pc in pcc) {
  751. PoolConnectionFactory.CheckPoolConnection (pc);
  752. }
  753. List<Client> cc = new List<Client> (clients.Values);
  754. foreach (Client c in cc) {
  755. try {
  756. if ((now - c.Created).TotalSeconds > GraceConnectionTime) {
  757. if (c.PoolConnection == null || c.PoolConnection.TcpClient == null) DisconnectClient (c, "timeout.");
  758. else if (!c.PoolConnection.TcpClient.Connected) DisconnectClient (c, "lost pool connection.");
  759. else if ((now - c.LastPoolJobTime).TotalSeconds > PoolTimeout) {
  760. DisconnectClient (c, "pool is not sending new jobs.");
  761. }
  762. }
  763. } catch { RemoveClient (c.WebSocket.ConnectionInfo.Id); }
  764. }
  765. if (clients.ContainsKey (Guid.Empty)) {
  766. if (clients.Count == 1)
  767. RemoveClient (Guid.Empty);
  768. } else {
  769. // we removed ourself because we got disconnected from the pool
  770. // make us alive again!
  771. if (clients.Count > 0) {
  772. Console.WriteLine ("disconnected from own pool. trying to reconnect.");
  773. ownJob = new Job ();
  774. CreateOurself ();
  775. }
  776. }
  777. HashesCheckedThisHeartbeat = 0;
  778. if (Hearbeats % ForceGCEveryXHeartbeat == 0) {
  779. Console.WriteLine ("Garbage collection. Currently using {0} MB.", Math.Round (((double) (GC.GetTotalMemory (false)) / 1024 / 1024)));
  780. DateTime tbc = DateTime.Now;
  781. // trust me, I am a professional
  782. GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced); // DON'T DO THIS!!!
  783. Console.WriteLine ("Garbage collected in {0} ms. Currently using {1} MB ({2} clients).", (DateTime.Now - tbc).Milliseconds,
  784. Math.Round (((double) (GC.GetTotalMemory (false)) / 1024 / 1024)), clients.Count);
  785. }
  786. } catch (Exception ex) {
  787. Console.WriteLine ("{0} Exception caught in the main loop !", ex);
  788. }
  789. }
  790. }
  791. }
  792. }