PageRenderTime 84ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/MonoTorrent/MonoTorrent.Client/ClientEngine.cs

http://github.com/mono/monotorrent
C# | 635 lines | 411 code | 128 blank | 96 comment | 59 complexity | da44060027f08514adf6fc6a808cc896 MD5 | raw file
Possible License(s): MIT
  1. //
  2. // ClientEngine.cs
  3. //
  4. // Authors:
  5. // Alan McGovern alan.mcgovern@gmail.com
  6. //
  7. // Copyright (C) 2006 Alan McGovern
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining
  10. // a copy of this software and associated documentation files (the
  11. // "Software"), to deal in the Software without restriction, including
  12. // without limitation the rights to use, copy, modify, merge, publish,
  13. // distribute, sublicense, and/or sell copies of the Software, and to
  14. // permit persons to whom the Software is furnished to do so, subject to
  15. // the following conditions:
  16. //
  17. // The above copyright notice and this permission notice shall be
  18. // included in all copies or substantial portions of the Software.
  19. //
  20. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  21. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  22. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  23. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  24. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  25. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  26. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  27. //
  28. using System;
  29. using System.Collections.Generic;
  30. using System.Collections.ObjectModel;
  31. using System.ComponentModel;
  32. using System.IO;
  33. using System.Linq;
  34. using System.Net;
  35. using System.Text;
  36. using System.Threading;
  37. using System.Threading.Tasks;
  38. using MonoTorrent.BEncoding;
  39. using MonoTorrent.Client.Listeners;
  40. using MonoTorrent.Client.PieceWriters;
  41. using MonoTorrent.Client.PortForwarding;
  42. using MonoTorrent.Client.RateLimiters;
  43. using MonoTorrent.Dht;
  44. namespace MonoTorrent.Client
  45. {
  46. /// <summary>
  47. /// The Engine that contains the TorrentManagers
  48. /// </summary>
  49. public class ClientEngine : IDisposable
  50. {
  51. internal static readonly MainLoop MainLoop = new MainLoop ("Client Engine Loop");
  52. /// <summary>
  53. /// An un-seeded random number generator which will not generate the same
  54. /// random sequence when the application is restarted.
  55. /// </summary>
  56. static readonly Random PeerIdRandomGenerator = new Random ();
  57. #region Global Constants
  58. // This is the number of 16kB requests which can be queued against one peer.
  59. internal static readonly int DefaultMaxPendingRequests = 256;
  60. public static readonly bool SupportsInitialSeed = false;
  61. public static readonly bool SupportsLocalPeerDiscovery = true;
  62. public static readonly bool SupportsWebSeed = true;
  63. public static readonly bool SupportsExtended = true;
  64. public static readonly bool SupportsFastPeer = true;
  65. public static readonly bool SupportsEncryption = true;
  66. public static readonly bool SupportsEndgameMode = true;
  67. public static readonly bool SupportsDht = true;
  68. internal const int TickLength = 500; // A logic tick will be performed every TickLength miliseconds
  69. #endregion
  70. #region Events
  71. public event EventHandler<StatsUpdateEventArgs> StatsUpdate;
  72. public event EventHandler<CriticalExceptionEventArgs> CriticalException;
  73. #endregion
  74. #region Member Variables
  75. internal static readonly BufferPool BufferPool = new BufferPool ();
  76. readonly ListenManager listenManager; // Listens for incoming connections and passes them off to the correct TorrentManager
  77. int tickCount;
  78. /// <summary>
  79. /// The <see cref="TorrentManager"/> instances registered by the user.
  80. /// </summary>
  81. readonly List<TorrentManager> publicTorrents;
  82. /// <summary>
  83. /// The <see cref="TorrentManager"/> instances registered by the user and the instances
  84. /// implicitly created by <see cref="DownloadMetadataAsync(MagnetLink, CancellationToken)"/>.
  85. /// </summary>
  86. readonly List<TorrentManager> allTorrents;
  87. readonly RateLimiter uploadLimiter;
  88. readonly RateLimiterGroup uploadLimiters;
  89. readonly RateLimiter downloadLimiter;
  90. readonly RateLimiterGroup downloadLimiters;
  91. #endregion
  92. #region Properties
  93. public ConnectionManager ConnectionManager { get; }
  94. public IDhtEngine DhtEngine { get; private set; }
  95. public DiskManager DiskManager { get; }
  96. public bool Disposed { get; private set; }
  97. /// <summary>
  98. /// Returns true when <see cref="EnablePortForwardingAsync"/> is invoked. When enabled, the
  99. /// engine will automatically forward ports using uPnP and/or NAT-PMP compatible routers.
  100. /// </summary>
  101. public bool PortForwardingEnabled => PortForwarder.Active;
  102. public IPeerListener Listener { get; }
  103. public ILocalPeerDiscovery LocalPeerDiscovery { get; private set; }
  104. /// <summary>
  105. /// When <see cref="PortForwardingEnabled"/> is set to true, this will return a representation
  106. /// of the ports the engine is managing.
  107. /// </summary>
  108. public Mappings PortMappings => PortForwardingEnabled ? PortForwarder.Mappings : Mappings.Empty;
  109. public bool IsRunning { get; private set; }
  110. public BEncodedString PeerId { get; }
  111. internal IPortForwarder PortForwarder { get; }
  112. public EngineSettings Settings { get; }
  113. public IList<TorrentManager> Torrents { get; }
  114. public long TotalDownloadSpeed {
  115. get {
  116. long total = 0;
  117. for (int i = 0; i < publicTorrents.Count; i++)
  118. total += publicTorrents[i].Monitor.DownloadSpeed;
  119. return total;
  120. }
  121. }
  122. public long TotalUploadSpeed {
  123. get {
  124. long total = 0;
  125. for (int i = 0; i < publicTorrents.Count; i++)
  126. total += publicTorrents[i].Monitor.UploadSpeed;
  127. return total;
  128. }
  129. }
  130. #endregion
  131. #region Constructors
  132. public ClientEngine ()
  133. : this(new EngineSettings ())
  134. {
  135. }
  136. public ClientEngine (EngineSettings settings)
  137. : this (settings, new DiskWriter ())
  138. {
  139. }
  140. public ClientEngine (EngineSettings settings, IPieceWriter writer)
  141. : this (settings, new PeerListener (new IPEndPoint (IPAddress.Any, settings.ListenPort)), writer)
  142. {
  143. }
  144. public ClientEngine (EngineSettings settings, IPeerListener listener)
  145. : this (settings, listener, new DiskWriter ())
  146. {
  147. }
  148. public ClientEngine (EngineSettings settings, IPeerListener listener, IPieceWriter writer)
  149. {
  150. Check.Settings (settings);
  151. Check.Listener (listener);
  152. Check.Writer (writer);
  153. // This is just a sanity check to make sure the ReusableTasks.dll assembly is
  154. // loadable.
  155. GC.KeepAlive (ReusableTasks.ReusableTask.CompletedTask);
  156. PeerId = GeneratePeerId ();
  157. Listener = listener ?? throw new ArgumentNullException (nameof (listener));
  158. Settings = settings ?? throw new ArgumentNullException (nameof (settings));
  159. allTorrents = new List<TorrentManager> ();
  160. publicTorrents = new List<TorrentManager> ();
  161. Torrents = new ReadOnlyCollection<TorrentManager> (publicTorrents);
  162. DiskManager = new DiskManager (Settings, writer);
  163. ConnectionManager = new ConnectionManager (PeerId, Settings, DiskManager);
  164. DhtEngine = new NullDhtEngine ();
  165. listenManager = new ListenManager (this);
  166. PortForwarder = new MonoNatPortForwarder ();
  167. MainLoop.QueueTimeout (TimeSpan.FromMilliseconds (TickLength), delegate {
  168. if (IsRunning && !Disposed)
  169. LogicTick ();
  170. return !Disposed;
  171. });
  172. downloadLimiter = new RateLimiter ();
  173. downloadLimiters = new RateLimiterGroup {
  174. new DiskWriterLimiter(DiskManager),
  175. downloadLimiter,
  176. };
  177. uploadLimiter = new RateLimiter ();
  178. uploadLimiters = new RateLimiterGroup {
  179. uploadLimiter
  180. };
  181. listenManager.Register (listener);
  182. if (SupportsLocalPeerDiscovery)
  183. RegisterLocalPeerDiscovery (new LocalPeerDiscovery (Settings));
  184. }
  185. #endregion
  186. #region Methods
  187. void CheckDisposed ()
  188. {
  189. if (Disposed)
  190. throw new ObjectDisposedException (GetType ().Name);
  191. }
  192. public bool Contains (InfoHash infoHash)
  193. {
  194. CheckDisposed ();
  195. if (infoHash == null)
  196. return false;
  197. return publicTorrents.Exists (m => m.InfoHash.Equals (infoHash));
  198. }
  199. public bool Contains (Torrent torrent)
  200. {
  201. CheckDisposed ();
  202. if (torrent == null)
  203. return false;
  204. return Contains (torrent.InfoHash);
  205. }
  206. public bool Contains (TorrentManager manager)
  207. {
  208. CheckDisposed ();
  209. if (manager == null)
  210. return false;
  211. return Contains (manager.Torrent);
  212. }
  213. public void Dispose ()
  214. {
  215. if (Disposed)
  216. return;
  217. Disposed = true;
  218. MainLoop.QueueWait (() => {
  219. DhtEngine.Dispose ();
  220. DiskManager.Dispose ();
  221. listenManager.Dispose ();
  222. LocalPeerDiscovery.Stop ();
  223. });
  224. }
  225. /// <summary>
  226. /// Downloads the .torrent metadata for the provided MagnetLink.
  227. /// </summary>
  228. /// <param name="magnetLink">The MagnetLink to get the metadata for.</param>
  229. /// <param name="token">The cancellation token used to to abort the download. This method will
  230. /// only complete if the metadata successfully downloads, or the token is cancelled.</param>
  231. /// <returns></returns>
  232. public async Task<byte[]> DownloadMetadataAsync (MagnetLink magnetLink, CancellationToken token)
  233. {
  234. var manager = new TorrentManager (magnetLink);
  235. var metadataCompleted = new TaskCompletionSource<byte[]> ();
  236. using var registration = token.Register (() => metadataCompleted.TrySetResult (null));
  237. manager.MetadataReceived += (o, e) => metadataCompleted.TrySetResult (e.dict);
  238. await Register (manager, isPublic: false);
  239. await manager.StartAsync (metadataOnly: true);
  240. var data = await metadataCompleted.Task;
  241. await manager.StopAsync ();
  242. await Unregister (manager);
  243. token.ThrowIfCancellationRequested ();
  244. return data;
  245. }
  246. async void HandleLocalPeerFound (object sender, LocalPeerFoundEventArgs args)
  247. {
  248. try {
  249. await MainLoop;
  250. TorrentManager manager = allTorrents.FirstOrDefault (t => t.InfoHash == args.InfoHash);
  251. // There's no TorrentManager in the engine
  252. if (manager == null)
  253. return;
  254. // The torrent is marked as private, so we can't add random people
  255. if (manager.HasMetadata && manager.Torrent.IsPrivate) {
  256. manager.RaisePeersFound (new LocalPeersAdded (manager, 0, 0));
  257. } else {
  258. // Add new peer to matched Torrent
  259. var peer = new Peer ("", args.Uri);
  260. int peersAdded = manager.AddPeer (peer, fromTrackers: false, prioritise: true) ? 1 : 0;
  261. manager.RaisePeersFound (new LocalPeersAdded (manager, peersAdded, 1));
  262. }
  263. } catch {
  264. // We don't care if the peer couldn't be added (for whatever reason)
  265. }
  266. }
  267. public async Task PauseAll ()
  268. {
  269. CheckDisposed ();
  270. await MainLoop;
  271. var tasks = new List<Task> ();
  272. foreach (TorrentManager manager in publicTorrents)
  273. tasks.Add (manager.PauseAsync ());
  274. await Task.WhenAll (tasks);
  275. }
  276. public async Task Register (TorrentManager manager)
  277. => await Register (manager, true);
  278. async Task Register (TorrentManager manager, bool isPublic)
  279. {
  280. CheckDisposed ();
  281. Check.Manager (manager);
  282. await MainLoop;
  283. if (manager.Engine != null)
  284. throw new TorrentException ("This manager has already been registered");
  285. if (Contains (manager.Torrent))
  286. throw new TorrentException ("A manager for this torrent has already been registered");
  287. allTorrents.Add (manager);
  288. if (isPublic)
  289. publicTorrents.Add (manager);
  290. ConnectionManager.Add (manager);
  291. listenManager.Add (manager.InfoHash);
  292. manager.Engine = this;
  293. manager.DownloadLimiters.Add (downloadLimiters);
  294. manager.UploadLimiters.Add (uploadLimiters);
  295. if (DhtEngine != null && manager.Torrent?.Nodes != null && DhtEngine.State != DhtState.Ready) {
  296. try {
  297. DhtEngine.Add (manager.Torrent.Nodes);
  298. } catch {
  299. // FIXME: Should log this somewhere, though it's not critical
  300. }
  301. }
  302. }
  303. public async Task RegisterDhtAsync (IDhtEngine engine)
  304. {
  305. await MainLoop;
  306. if (DhtEngine != null) {
  307. DhtEngine.StateChanged -= DhtEngineStateChanged;
  308. DhtEngine.PeersFound -= DhtEnginePeersFound;
  309. await DhtEngine.StopAsync ();
  310. DhtEngine.Dispose ();
  311. }
  312. DhtEngine = engine ?? new NullDhtEngine ();
  313. DhtEngine.StateChanged += DhtEngineStateChanged;
  314. DhtEngine.PeersFound += DhtEnginePeersFound;
  315. }
  316. public async Task RegisterLocalPeerDiscoveryAsync (ILocalPeerDiscovery localPeerDiscovery)
  317. {
  318. await MainLoop;
  319. RegisterLocalPeerDiscovery (localPeerDiscovery);
  320. }
  321. internal void RegisterLocalPeerDiscovery (ILocalPeerDiscovery localPeerDiscovery)
  322. {
  323. if (LocalPeerDiscovery != null) {
  324. LocalPeerDiscovery.PeerFound -= HandleLocalPeerFound;
  325. LocalPeerDiscovery.Stop ();
  326. }
  327. LocalPeerDiscovery = localPeerDiscovery ?? new NullLocalPeerDiscovery ();
  328. if (LocalPeerDiscovery != null) {
  329. LocalPeerDiscovery.PeerFound += HandleLocalPeerFound;
  330. LocalPeerDiscovery.Start ();
  331. }
  332. }
  333. async void DhtEnginePeersFound (object o, PeersFoundEventArgs e)
  334. {
  335. await MainLoop;
  336. TorrentManager manager = allTorrents.FirstOrDefault (t => t.InfoHash == e.InfoHash);
  337. if (manager == null)
  338. return;
  339. if (manager.CanUseDht) {
  340. int successfullyAdded = await manager.AddPeersAsync (e.Peers);
  341. manager.RaisePeersFound (new DhtPeersAdded (manager, successfullyAdded, e.Peers.Count));
  342. } else {
  343. // This is only used for unit testing to validate that even if the DHT engine
  344. // finds peers for a private torrent, we will not add them to the manager.
  345. manager.RaisePeersFound (new DhtPeersAdded (manager, 0, 0));
  346. }
  347. }
  348. async void DhtEngineStateChanged (object o, EventArgs e)
  349. {
  350. if (DhtEngine.State != DhtState.Ready)
  351. return;
  352. await MainLoop;
  353. foreach (TorrentManager manager in allTorrents) {
  354. if (!manager.CanUseDht)
  355. continue;
  356. if (Listener is ISocketListener listener)
  357. DhtEngine.Announce (manager.InfoHash, listener.EndPoint.Port);
  358. else
  359. DhtEngine.Announce (manager.InfoHash, Settings.ListenPort);
  360. DhtEngine.GetPeers (manager.InfoHash);
  361. }
  362. }
  363. [EditorBrowsable (EditorBrowsableState.Never)]
  364. public Task StartAll ()
  365. {
  366. return StartAllAsync ();
  367. }
  368. public async Task StartAllAsync ()
  369. {
  370. CheckDisposed ();
  371. await MainLoop;
  372. var tasks = new List<Task> ();
  373. for (int i = 0; i < publicTorrents.Count; i++)
  374. tasks.Add (publicTorrents[i].StartAsync ());
  375. await Task.WhenAll (tasks);
  376. }
  377. [EditorBrowsable (EditorBrowsableState.Never)]
  378. public Task StopAll ()
  379. {
  380. return StopAllAsync ();
  381. }
  382. /// <summary>
  383. /// Stops all active <see cref="TorrentManager"/> instances.
  384. /// </summary>
  385. /// <returns></returns>
  386. public Task StopAllAsync ()
  387. {
  388. return StopAllAsync (Timeout.InfiniteTimeSpan);
  389. }
  390. /// <summary>
  391. /// Stops all active <see cref="TorrentManager"/> instances. The final announce for each <see cref="TorrentManager"/> will be limited
  392. /// to the maximum of either 2 seconds or <paramref name="timeout"/> seconds.
  393. /// </summary>
  394. /// <param name="timeout">The timeout for the final tracker announce.</param>
  395. /// <returns></returns>
  396. public async Task StopAllAsync (TimeSpan timeout)
  397. {
  398. CheckDisposed ();
  399. await MainLoop;
  400. var tasks = new List<Task> ();
  401. for (int i = 0; i < publicTorrents.Count; i++)
  402. tasks.Add (publicTorrents[i].StopAsync (timeout));
  403. await Task.WhenAll (tasks);
  404. }
  405. public async Task Unregister (TorrentManager manager)
  406. {
  407. CheckDisposed ();
  408. Check.Manager (manager);
  409. await MainLoop;
  410. if (manager.Engine != this)
  411. throw new TorrentException ("The manager has not been registered with this engine");
  412. if (manager.State != TorrentState.Stopped)
  413. throw new TorrentException ("The manager must be stopped before it can be unregistered");
  414. allTorrents.Remove (manager);
  415. publicTorrents.Remove (manager);
  416. ConnectionManager.Remove (manager);
  417. listenManager.Remove (manager.InfoHash);
  418. manager.Engine = null;
  419. manager.DownloadLimiters.Remove (downloadLimiters);
  420. manager.UploadLimiters.Remove (uploadLimiters);
  421. }
  422. #endregion
  423. #region Private/Internal methods
  424. void LogicTick ()
  425. {
  426. tickCount++;
  427. if (tickCount % 2 == 0) {
  428. downloadLimiter.UpdateChunks (Settings.MaximumDownloadSpeed, TotalDownloadSpeed);
  429. uploadLimiter.UpdateChunks (Settings.MaximumUploadSpeed, TotalUploadSpeed);
  430. }
  431. ConnectionManager.CancelPendingConnects ();
  432. ConnectionManager.TryConnect ();
  433. DiskManager.Tick ();
  434. for (int i = 0; i < allTorrents.Count; i++)
  435. allTorrents[i].Mode.Tick (tickCount);
  436. RaiseStatsUpdate (new StatsUpdateEventArgs ());
  437. }
  438. internal void RaiseCriticalException (CriticalExceptionEventArgs e)
  439. {
  440. CriticalException?.InvokeAsync (this, e);
  441. }
  442. internal void RaiseStatsUpdate (StatsUpdateEventArgs args)
  443. {
  444. StatsUpdate?.InvokeAsync (this, args);
  445. }
  446. internal async Task StartAsync ()
  447. {
  448. CheckDisposed ();
  449. if (!IsRunning) {
  450. IsRunning = true;
  451. if (Listener.Status == ListenerStatus.NotListening)
  452. Listener.Start ();
  453. await PortForwarder.RegisterMappingAsync (new Mapping (Protocol.Tcp, Settings.ListenPort));
  454. }
  455. }
  456. /// <summary>
  457. /// Sets <see cref="PortForwardingEnabled"/> to true and begins searching for uPnP or
  458. /// NAT-PMP compatible devices. If any are discovered they will be used to forward the
  459. /// ports used by the engine.
  460. /// </summary>
  461. /// <param name="token">If the token is cancelled and an <see cref="OperationCanceledException"/>
  462. /// is thrown then the engine is guaranteed to not be searching for compatible devices.</param>
  463. /// <returns></returns>
  464. public async Task EnablePortForwardingAsync (CancellationToken token)
  465. {
  466. await PortForwarder.StartAsync (token);
  467. }
  468. /// <summary>
  469. /// Sets <see cref="PortForwardingEnabled"/> to false and the engine will no longer
  470. /// seach for uPnP or NAT-PMP compatible devices. Ports forwarding requests will
  471. /// be deleted, where possible.
  472. /// </summary>
  473. /// <param name="token">If the token is cancelled the engine is guaranteed to no longer search
  474. /// for compatible devices, but existing port forwarding requests may not be deleted.</param>
  475. /// <returns></returns>
  476. public async Task DisablePortForwardingAsync (CancellationToken token)
  477. {
  478. await PortForwarder.StopAsync (true, token);
  479. }
  480. internal async Task StopAsync ()
  481. {
  482. CheckDisposed ();
  483. // If all the torrents are stopped, stop ticking
  484. IsRunning = allTorrents.Exists (m => m.State != TorrentState.Stopped);
  485. if (!IsRunning) {
  486. Listener.Stop ();
  487. await PortForwarder.UnregisterMappingAsync (new Mapping (Protocol.Tcp, Settings.ListenPort), CancellationToken.None);
  488. }
  489. }
  490. static BEncodedString GeneratePeerId ()
  491. {
  492. var sb = new StringBuilder (20);
  493. sb.Append ("-");
  494. sb.Append (VersionInfo.ClientVersion);
  495. sb.Append ("-");
  496. // Create and use a single Random instance which *does not* use a seed so that
  497. // the random sequence generated is definitely not the same between application
  498. // restarts.
  499. lock (PeerIdRandomGenerator) {
  500. while (sb.Length < 20)
  501. sb.Append (PeerIdRandomGenerator.Next (0, 9));
  502. }
  503. return new BEncodedString (sb.ToString ());
  504. }
  505. #endregion
  506. }
  507. }