From 05cfb1e0f42b2a82150715cdcbae22b6ecc19f69 Mon Sep 17 00:00:00 2001 From: SuRGeoNix Date: Mon, 23 Nov 2020 16:59:36 +0200 Subject: [PATCH] Updating to v2.3 - Implementing Save & Load/Restore Session (with Autonomous Advanced Part Files - APF) - Implementing Export & Load Configuration (for BitSwarm's Options) - Implementing Single Open for all BitSwarm's supported inputs (Torrent, Magnet, SHA1 Hash, Base32 Hash & Session) - Organizing Folder/Files - Adding more folder paths options (Completed, Incompleted .apf, .Torrent, Session .bsf) - Adding support for Env variables to be used with paths (format %var% even for unix platforms) - Namespaces, Assemblies & Code Clean-up - Console App: Adding new folder options & config load/export (both file config & command-line options can work together) - Console App: Trying for yet another time to fix views/output for all platforms - GUI App: Fixed some issues, have not updated properly yet with the new changes --- .../BitSwarm (Console Core Demo).csproj | 10 +- BitSwarm (Console Core Demo)/Options.cs | 154 +++ BitSwarm (Console Core Demo)/Program.cs | 198 ++-- BitSwarm (WinForms Demo)/App.config | 4 +- .../BitSwarm (WinForms Demo).csproj | 25 +- .../Properties/AssemblyInfo.cs | 6 +- BitSwarm (WinForms Demo)/frmMain.cs | 40 +- BitSwarm (WinForms Demo)/packages.config | 1 + BitSwarm/{BitField.cs => BEP/Bitfield.cs} | 34 +- BitSwarm/{ => BEP}/DHT.cs | 4 +- BitSwarm/{ => BEP}/Peer.cs | 23 +- BitSwarm/BEP/Torrent.cs | 505 +++++++++ BitSwarm/{ => BEP}/Tracker.cs | 2 +- BitSwarm/BitSwarm.cs | 968 ++++++++---------- BitSwarm/BitSwarm.csproj | 20 +- BitSwarm/Logger.cs | 4 +- BitSwarm/Options.cs | 106 ++ BitSwarm/PartFile.cs | 260 ----- BitSwarm/Stats.cs | 57 ++ BitSwarm/ThreadPool.cs | 138 +++ BitSwarm/Torrent.cs | 331 ------ BitSwarm/Utils.cs | 3 + 22 files changed, 1542 insertions(+), 1351 deletions(-) create mode 100644 BitSwarm (Console Core Demo)/Options.cs rename BitSwarm/{BitField.cs => BEP/Bitfield.cs} (93%) rename BitSwarm/{ => BEP}/DHT.cs (99%) rename BitSwarm/{ => BEP}/Peer.cs (98%) create mode 100644 BitSwarm/BEP/Torrent.cs rename BitSwarm/{ => BEP}/Tracker.cs (99%) create mode 100644 BitSwarm/Options.cs delete mode 100644 BitSwarm/PartFile.cs create mode 100644 BitSwarm/Stats.cs create mode 100644 BitSwarm/ThreadPool.cs delete mode 100644 BitSwarm/Torrent.cs diff --git a/BitSwarm (Console Core Demo)/BitSwarm (Console Core Demo).csproj b/BitSwarm (Console Core Demo)/BitSwarm (Console Core Demo).csproj index 4274394..c9df9da 100644 --- a/BitSwarm (Console Core Demo)/BitSwarm (Console Core Demo).csproj +++ b/BitSwarm (Console Core Demo)/BitSwarm (Console Core Demo).csproj @@ -3,11 +3,11 @@ Exe netcoreapp3.1 - BitSwarmConsole - bswarm - 2.2.8.0 - 2.2.8.0 - 2.2.8 + SuRGeoNix.BitSwarmConsole + bitswarm_xxx + 2.3.0.0 + 2.3.0.0 + 2.3.0 SuRGeoNix BitSwarm BitSwarmConsole diff --git a/BitSwarm (Console Core Demo)/Options.cs b/BitSwarm (Console Core Demo)/Options.cs new file mode 100644 index 0000000..313da34 --- /dev/null +++ b/BitSwarm (Console Core Demo)/Options.cs @@ -0,0 +1,154 @@ +using CommandLine; + +using BitSwarmOptions = SuRGeoNix.BitSwarmLib.Options; + +namespace SuRGeoNix.BitSwarmConsole +{ + public class Options + { + // NOTE: For bool variables should have the opposite from BitSwarm's + + private static readonly int NOT_SET = -9999; + + + [Option("fc", HelpText = "Folder for completed files (Default: .)")] + public string FolderComplete { get; set; } + [Option("fi", HelpText = "Folder for .apf incompleted files (Default: %temp%/BitSwarm/.data)")] + public string FolderIncomplete{ get; set; } + [Option("ft", HelpText = "Folder for .torrent files (Default: %temp%/BitSwarm/.torrents)")] + public string FolderTorrents { get; set; } + [Option("fs", HelpText = "Folder for .bsf session files (Default: %temp%/BitSwarm/.sessions)")] + public string FolderSessions { get; set; } + + [Option("mc", HelpText = "Max new connection threads")] + public int MinThreads { get; set; } = NOT_SET; + + [Option("mt", HelpText = "Max total threads")] + public int MaxThreads { get; set; } = NOT_SET; + + [Option("boostmin", HelpText = "Boost new connection threads")] + public int BoostThreads { get; set; } = NOT_SET; + + [Option("boostsecs", HelpText = "Boost time in seconds")] + public int BoostTime { get; set; } = NOT_SET; + [Option("sleep", HelpText = "Sleep activation at this down rate KB/s (-1: Automatic | 0: Disabled)")] + public int SleepModeLimit { get; set; } = NOT_SET; + + [Option("no-dht", HelpText = "Disable DHT")] + public bool DisableDHT { get; set; } + + [Option("no-pex", HelpText = "Disable PEX")] + public bool DisablePEX { get; set; } + + [Option("no-trackers", HelpText = "Disable Trackers")] + public bool DisableTrackers { get; set; } + + [Option("trackers-num", HelpText = "# of peers will be requested from each tracker")] + public int PeersFromTracker{ get; set; } = NOT_SET; + + [Option("trackers-file",HelpText = "Trackers file to include (format 'scheme://host:port' per line)")] + public string TrackersPath { get; set; } + + [Option("ct", HelpText = "Connection timeout in ms")] + public int ConnectionTimeout{get; set; } = NOT_SET; + + [Option("ht", HelpText = "Handshake timeout in ms")] + public int HandshakeTimeout{ get; set; } = NOT_SET; + + [Option("pt", HelpText = "Piece timeout in ms")] + public int PieceTimeout { get; set; } = NOT_SET; + + [Option("pr", HelpText = "Piece retries")] + public int PieceRetries { get; set; } = NOT_SET; + + [Option("req-blocks", HelpText = "Parallel block requests per peer")] + public int BlockRequests { get; set; } = NOT_SET; + + [Option("log", HelpText = "Log verbosity [0-4]")] + public int LogVerbosity { get; set; } = NOT_SET; + + [Option("log-dht", HelpText = "Enable logging for DHT")] + public bool LogDHT { get; set; } + + [Option("log-peers", HelpText = "Enable logging for Peers")] + public bool LogPeer { get; set; } + + [Option("log-trackers", HelpText = "Enable logging for Trackers")] + public bool LogTracker { get; set; } + + [Option("log-stats", HelpText = "Enable logging for Stats")] + public bool LogStats { get; set; } + + [Value(0, MetaName = "Torrent|Magnet|Hash|Session", Required = true, HelpText = "")] + public string Input { get; set; } + + public static bool ParseOptionsToBitSwarm(Options userOptions, ref BitSwarmOptions bitSwarmOptions) + { + if (userOptions.FolderComplete != null) + bitSwarmOptions.FolderComplete = userOptions.FolderComplete; + + if (userOptions.FolderIncomplete != null) + bitSwarmOptions.FolderIncomplete = userOptions.FolderIncomplete; + + if (userOptions.FolderTorrents != null) + bitSwarmOptions.FolderTorrents = userOptions.FolderTorrents; + + if (userOptions.FolderSessions != null) + bitSwarmOptions.FolderSessions = userOptions.FolderSessions; + + if (userOptions.TrackersPath != null) + bitSwarmOptions.TrackersPath = userOptions.TrackersPath; + + if (userOptions.MinThreads != NOT_SET) + bitSwarmOptions.MinThreads = userOptions.MinThreads; + if (userOptions.MaxThreads != NOT_SET) + bitSwarmOptions.MaxThreads = userOptions.MaxThreads; + + if (userOptions.BoostThreads != NOT_SET) + bitSwarmOptions.BoostThreads = userOptions.BoostThreads; + if (userOptions.BoostTime != NOT_SET) + bitSwarmOptions.BoostTime = userOptions.BoostTime; + if (userOptions.SleepModeLimit != NOT_SET) + bitSwarmOptions.SleepModeLimit = userOptions.SleepModeLimit; + + if (userOptions.ConnectionTimeout != NOT_SET) + bitSwarmOptions.ConnectionTimeout = userOptions.ConnectionTimeout; + if (userOptions.HandshakeTimeout != NOT_SET) + bitSwarmOptions.HandshakeTimeout = userOptions.HandshakeTimeout; + if (userOptions.PieceTimeout != NOT_SET) + bitSwarmOptions.PieceTimeout = userOptions.PieceTimeout; + if (userOptions.PieceRetries != NOT_SET) + bitSwarmOptions.PieceRetries = userOptions.PieceRetries; + + if (userOptions.SleepModeLimit != NOT_SET) + bitSwarmOptions.PeersFromTracker = userOptions.PeersFromTracker; + + if (userOptions.DisableDHT) + bitSwarmOptions.EnableDHT = !userOptions.DisableDHT; + if (userOptions.DisablePEX) + bitSwarmOptions.EnablePEX = !userOptions.DisablePEX; + if (userOptions.DisableTrackers) + bitSwarmOptions.EnableTrackers = !userOptions.DisableTrackers; + + if (userOptions.BlockRequests != NOT_SET) + bitSwarmOptions.BlockRequests = userOptions.BlockRequests; + + if (userOptions.LogVerbosity != NOT_SET) + bitSwarmOptions.Verbosity = userOptions.LogVerbosity; + + if (bitSwarmOptions.Verbosity > 0) + { + if (userOptions.LogDHT) + bitSwarmOptions.LogDHT = userOptions.LogDHT; + if (userOptions.LogPeer) + bitSwarmOptions.LogPeer = userOptions.LogPeer; + if (userOptions.LogTracker) + bitSwarmOptions.LogTracker = userOptions.LogTracker; + if (userOptions.LogStats) + bitSwarmOptions.LogStats = userOptions.LogStats; + } + + return true; + } + } +} \ No newline at end of file diff --git a/BitSwarm (Console Core Demo)/Program.cs b/BitSwarm (Console Core Demo)/Program.cs index bee2574..97b49ed 100644 --- a/BitSwarm (Console Core Demo)/Program.cs +++ b/BitSwarm (Console Core Demo)/Program.cs @@ -1,26 +1,27 @@ using System; using System.Collections.Generic; -using System.IO; using CommandLine; using CommandLine.Text; -using SuRGeoNix; -using SuRGeoNix.BEP; +using SuRGeoNix.BitSwarmLib; +using SuRGeoNix.BitSwarmLib.BEP; -namespace BitSwarmConsole +using BitSwarmOptions = SuRGeoNix.BitSwarmLib.Options; + +namespace SuRGeoNix.BitSwarmConsole { class Program { - static BitSwarm bitSwarm; - static BitSwarm.DefaultOptions opt; - static Torrent torrent; + static BitSwarm bitSwarm; + static BitSwarmOptions bitSwarmOptions; + static Torrent torrent; - static bool sessionFinished; - static int prevHeight; - static object lockRefresh = new object(); + static bool sessionFinished; + static int prevHeight; + static object lockRefresh = new object(); - static View view = View.Stats; + static View view = View.Stats; enum View { @@ -28,57 +29,49 @@ enum View Stats, Torrent } - private static void Run(Options opt2) + + private static void Main(string[] args) + { + var parser = new Parser(with => with.HelpWriter = null); + var parserResult = parser.ParseArguments(args); + parserResult.WithParsed(options => Run(options)).WithNotParsed(errs => PrintHelp(parserResult, errs)); + } + private static void PrintHelp(ParserResult result, IEnumerable errs) + { + var helpText = HelpText.AutoBuild(result, h => { return HelpText.DefaultParsingErrorsHandler(result, h); }, e => e, false, 160); + helpText.Heading = $"[BitSwarm v{BitSwarm.Version}]"; + helpText.AddPostOptionsText("\r\n" + "CONFIG: \t\t\t\t Creates config file with default options (Place it under -> . | $HOME/.bitswarm | /etc/bitswarm | %APPDATA%/BitSwarm)\r\n. \t\t\t\t If [OPTIONS] are defined will override config values (Notice: you can use env variables even on unix %var%)\r\n\r\n\t" + $"bitswarm config\r\n"); + helpText.AddPostOptionsText("\r\n" + "USAGE: \r\n\r\n\t" + $"bitswarm [OPTIONS] Torrent|Magnet|Hash|Session"); + Console.WriteLine(helpText); + } + private static void PrintMenu() { Console.WriteLine("[1: Stats] [2: Torrent] [3: Peers] [4: Peers (w/Refresh)] [Ctrl-C: Exit]".PadLeft(100, ' ')); } + + private static void Run(Options userOptions) { try { - // Prepare Options - opt = new BitSwarm.DefaultOptions(); - - opt.DownloadPath = opt2.DownloadPath; - - opt.MinThreads = opt2.MinThreads; - opt.MaxThreads = opt2.MaxThreads; + Console.WriteLine($"[BitSwarm v{BitSwarm.Version}] Initializing ..."); - opt.BoostThreads = opt2.BoostThreads; - opt.BoostTime = opt2.BoostTime; - opt.SleepModeLimit = opt2.SleepModeLimit; - - opt.ConnectionTimeout = opt2.ConnectionTimeout; - opt.HandshakeTimeout = opt2.HandshakeTimeout; - opt.PieceTimeout = opt2.PieceTimeout; - opt.PieceRetries = opt2.PieceRetries; - - opt.PeersFromTracker = opt2.PeersFromTracker; - opt.TrackersPath = opt2.TrackersPath; - - opt.EnableDHT = !opt2.DisableDHT; - opt.EnablePEX = !opt2.DisablePEX; - opt.EnableTrackers = !opt2.DisableTrackers; - - opt.BlockRequests = opt2.BlockRequests; - - opt.Verbosity = opt2.LogVerbosity; - if (opt.Verbosity > 0) + if (userOptions.Input == "config") { - opt.LogDHT = !opt2.NoLogDHT; - opt.LogPeer = !opt2.NoLogPeer; - opt.LogTracker = !opt2.NoLogTracker; - opt.LogStats = !opt2.NoLogStats; + BitSwarmOptions.CreateConfig(new BitSwarmOptions()); + Console.WriteLine($"[BitSwarm v{BitSwarm.Version}] Config {BitSwarmOptions.ConfigFile} created."); + return; } + else if ((bitSwarmOptions = BitSwarmOptions.LoadConfig()) != null) + Console.WriteLine($"[BitSwarm v{BitSwarm.Version}] Config {BitSwarmOptions.ConfigFile} loaded."); - // Initialize & Start BitSwarm - bitSwarm = new BitSwarm(opt); + if (bitSwarmOptions == null) bitSwarmOptions = new BitSwarmOptions(); + if (!Options.ParseOptionsToBitSwarm(userOptions, ref bitSwarmOptions)) return; + + // BitSwarm [Create | Subscribe | Open | Start] + bitSwarm = new BitSwarm(bitSwarmOptions); bitSwarm.MetadataReceived += BitSwarm_MetadataReceived; // Receives torrent data [on torrent file will fire directly, on magnetlink will fire on metadata received] bitSwarm.StatsUpdated += BitSwarm_StatsUpdated; // Stats refresh every 2 seconds bitSwarm.StatusChanged += BitSwarm_StatusChanged; // Paused/Stopped or Finished - if (File.Exists(opt2.TorrentFile)) - bitSwarm.Initiliaze(opt2.TorrentFile); - else - bitSwarm.Initiliaze(new Uri(opt2.TorrentFile)); - + bitSwarm.Open(userOptions.Input); bitSwarm.Start(); // Stats | Torrent | Peers Views [Until Stop or Finish] @@ -101,7 +94,6 @@ private static void Run(Options opt2) case ConsoleKey.D1: view = View.Stats; Console.Clear(); - Console.WriteLine(bitSwarm.DumpTorrent() + "\r\n"); Console.WriteLine(bitSwarm.DumpStats()); PrintMenu(); break; @@ -142,28 +134,18 @@ private static void Run(Options opt2) } catch (Exception e) { Console.WriteLine($"[ERROR] {e.Message}"); } } - private static void Main(string[] args) - { - var parser = new Parser(with => with.HelpWriter = null); - var parserResult = parser.ParseArguments(args); - parserResult.WithParsed(options => Run(options)).WithNotParsed(errs => PrintHelp(parserResult, errs)); - } - private static void PrintHelp(ParserResult result, IEnumerable errs) - { - var helpText = HelpText.AutoBuild(result, h => { h.Heading = $"BitSwarm v{BitSwarm.Version}" ; return HelpText.DefaultParsingErrorsHandler(result, h); }, e => e, false, 160); - helpText.AddPostOptionsText("\r\n" + "USAGE: \r\n\r\n\t" + $"{AppDomain.CurrentDomain.FriendlyName} [OPTIONS] torrentfile|magnetlink"); - Console.WriteLine(helpText); - } - private static void PrintMenu() { Console.WriteLine("[1: Stats] [2: Torrent] [3: Peers] [4: Peers (w/Refresh)] [Ctrl-C: Exit]".PadLeft(100, ' ')); } - private static void BitSwarm_StatusChanged(object source, BitSwarm.StatusChangedArgs e) { if (e.Status == 0 && torrent != null && torrent.file.name != null) - Console.WriteLine($"Download of {torrent.file.name} success!\r\n"); + { + Console.WriteLine($"\r\nDownload of {torrent.file.name} success!\r\n\r\n"); + Console.WriteLine(bitSwarm.DumpTorrent()); + Console.WriteLine($"\r\nDownload of {torrent.file.name} success!\r\n\r\n"); + } else if (e.Status == 2) - Console.WriteLine("An error has been occured :( " + e.ErrorMsg); + Console.WriteLine("An error has been occured :( \r\n" + e.ErrorMsg); - bitSwarm.Dispose(); + bitSwarm?.Dispose(); bitSwarm = null; sessionFinished = true; } @@ -172,8 +154,17 @@ private static void BitSwarm_MetadataReceived(object source, BitSwarm.MetadataRe lock (lockRefresh) { torrent = e.Torrent; + + view = View.Torrent; + Console.Clear(); + Console.WriteLine(bitSwarm.DumpTorrent()); + PrintMenu(); + + System.Threading.Thread.Sleep(3000); Console.Clear(); - Console.WriteLine(bitSwarm.DumpTorrent() + "\r\n"); + Console.WriteLine(bitSwarm.DumpStats()); + PrintMenu(); + view = View.Stats; } } private static void BitSwarm_StatsUpdated(object source, BitSwarm.StatsUpdatedArgs e) @@ -194,7 +185,6 @@ private static void BitSwarm_StatsUpdated(object source, BitSwarm.StatsUpdatedAr else if (view == View.Stats) { Console.SetCursorPosition(0, 0); - Console.WriteLine(bitSwarm.DumpTorrent() + "\r\n"); Console.WriteLine(bitSwarm.DumpStats()); } @@ -204,72 +194,4 @@ private static void BitSwarm_StatsUpdated(object source, BitSwarm.StatsUpdatedAr } catch (Exception) { } } } - - class Options - { - [Option('o', "output", Default = ".", HelpText = "Download directory")] - public string DownloadPath { get; set; } - - [Option("mc", Default = 15, HelpText = "Max new connection threads")] - public int MinThreads { get; set; } - - [Option("mt", Default = 150, HelpText = "Max total threads")] - public int MaxThreads { get; set; } - - [Option("boostmin", Default = 60, HelpText = "Boost new connection threads")] - public int BoostThreads { get; set; } - - [Option("boostsecs", Default = 30, HelpText = "Boost time in seconds")] - public int BoostTime { get; set; } - [Option("sleep", Default = 0, HelpText = "Sleep activation at this down rate KB/s (-1 Automatic)")] - public int SleepModeLimit { get; set; } - - [Option("no-dht", Default = false,HelpText = "Disable DHT")] - public bool DisableDHT { get; set; } - - [Option("no-pex", Default = false,HelpText = "Disable PEX")] - public bool DisablePEX { get; set; } - - [Option("no-trackers", Default = false,HelpText = "Disable Trackers")] - public bool DisableTrackers { get; set; } - - [Option("trackers-num", Default = -1, HelpText = "# of peers will be requested from each tracker")] - public int PeersFromTracker{ get; set; } - - [Option("trackers-file", HelpText = "Trackers file to include (format 'scheme://host:port' per line)")] - public string TrackersPath { get; set; } - - [Option("ct", Default = 600, HelpText = "Connection timeout in ms")] - public int ConnectionTimeout{get; set; } - - [Option("ht", Default = 800, HelpText = "Handshake timeout in ms")] - public int HandshakeTimeout{ get; set; } - - [Option("pt", Default = 5000, HelpText = "Piece timeout in ms")] - public int PieceTimeout { get; set; } - - [Option("pr", Default = 0, HelpText = "Piece retries")] - public int PieceRetries { get; set; } - - [Option("req-blocks", Default = 6, HelpText = "Parallel block requests per peer")] - public int BlockRequests { get; set; } - - [Option("log", Default = 0, HelpText = "Log verbosity [0-4]")] - public int LogVerbosity { get; set; } - - [Option("no-log-dht", Default = false,HelpText = "Disable logging for DHT")] - public bool NoLogDHT { get; set; } - - [Option("no-log-peers", Default = false,HelpText = "Disable logging for Peers")] - public bool NoLogPeer { get; set; } - - [Option("no-log-trackers",Default=false,HelpText = "Disable logging for Trackers")] - public bool NoLogTracker { get; set; } - - [Option("no-log-stats", Default = false,HelpText = "Disable logging for Stats")] - public bool NoLogStats { get; set; } - - [Value(0, MetaName = "Torrent file or Magnet link", Required = true, HelpText = "")] - public string TorrentFile { get; set; } - } } \ No newline at end of file diff --git a/BitSwarm (WinForms Demo)/App.config b/BitSwarm (WinForms Demo)/App.config index 89eb63c..e43c63b 100644 --- a/BitSwarm (WinForms Demo)/App.config +++ b/BitSwarm (WinForms Demo)/App.config @@ -1,7 +1,7 @@  - - + + diff --git a/BitSwarm (WinForms Demo)/BitSwarm (WinForms Demo).csproj b/BitSwarm (WinForms Demo)/BitSwarm (WinForms Demo).csproj index 21110fc..e4a70e6 100644 --- a/BitSwarm (WinForms Demo)/BitSwarm (WinForms Demo).csproj +++ b/BitSwarm (WinForms Demo)/BitSwarm (WinForms Demo).csproj @@ -7,7 +7,7 @@ {54478B08-BB57-4B2A-A2E4-0A9598EB8635} WinExe SuRGeoNix.BitSwarmClient - BitSwarmClient + BitSwarm v4.7.2 512 true @@ -60,6 +60,9 @@ ..\packages\BencodeNET.3.1.4\lib\netstandard2.0\BencodeNET.dll + + ..\packages\APF.1.1.0\lib\netstandard2.0\Partfiles.dll + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll @@ -112,6 +115,7 @@ Resources.resx True + SettingsSingleFileGenerator @@ -123,9 +127,6 @@ True - - - False @@ -152,16 +153,12 @@ - -if $(ConfigurationName) == Release ( -cd "$(TargetDir)" -del *.xml -del *.pdb -mkdir Libs -move *.dll Libs - -rename BitSwarmClient.exe BitSwarm.exe -rename BitSwarmClient.exe.config BitSwarm.exe.config + if $(ConfigurationName) == Release ( + cd "$(TargetDir)" + del *.xml + del *.pdb + mkdir Libs + move *.dll Libs ) diff --git a/BitSwarm (WinForms Demo)/Properties/AssemblyInfo.cs b/BitSwarm (WinForms Demo)/Properties/AssemblyInfo.cs index ada4968..7c58224 100644 --- a/BitSwarm (WinForms Demo)/Properties/AssemblyInfo.cs +++ b/BitSwarm (WinForms Demo)/Properties/AssemblyInfo.cs @@ -6,7 +6,7 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("BitSwarm")] -[assembly: AssemblyDescription("BitTorrent Client WinForms Demo")] +[assembly: AssemblyDescription("Bittorrent Client")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("SuRGeoNix")] [assembly: AssemblyProduct("BitSwarm")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.2.7.0")] -[assembly: AssemblyFileVersion("2.2.7.0")] +[assembly: AssemblyVersion("2.3.0.0")] +[assembly: AssemblyFileVersion("2.3.0.0")] diff --git a/BitSwarm (WinForms Demo)/frmMain.cs b/BitSwarm (WinForms Demo)/frmMain.cs index e3bdb6a..e8639d9 100644 --- a/BitSwarm (WinForms Demo)/frmMain.cs +++ b/BitSwarm (WinForms Demo)/frmMain.cs @@ -1,18 +1,18 @@ using System; using System.Globalization; -using System.IO; -using System.Windows.Forms; using System.Collections.Generic; +using System.Windows.Forms; -using SuRGeoNix.BEP; +using SuRGeoNix.BitSwarmLib; +using SuRGeoNix.BitSwarmLib.BEP; namespace SuRGeoNix.BitSwarmClient { public partial class frmMain : Form { - static Torrent torrent; - static BitSwarm bitSwarm; - static BitSwarm.DefaultOptions opt; + static Torrent torrent; + static BitSwarm bitSwarm; + static Options opt; long requestedBytes = 0; @@ -23,9 +23,9 @@ public frmMain() private void frmMain_Load(object sender, EventArgs e) { - BitSwarm.DefaultOptions opt = new BitSwarm.DefaultOptions(); + Options opt = new Options(); - downPath.Text = opt.DownloadPath; + downPath.Text = opt.FolderComplete; maxCon.Text = opt.MaxThreads.ToString(); maxThreads.Text = opt.MinThreads.ToString(); sleepLimit.Text = opt.SleepModeLimit.ToString(); @@ -45,9 +45,9 @@ private void button1_Click(object sender, EventArgs e) try { - opt = new BitSwarm.DefaultOptions(); + opt = new Options(); - opt.DownloadPath = downPath.Text; + opt.FolderComplete = downPath.Text; opt.MaxThreads = int.Parse(maxCon.Text); opt.MinThreads = int.Parse(maxThreads.Text); @@ -73,11 +73,7 @@ private void button1_Click(object sender, EventArgs e) bitSwarm.MetadataReceived += BitSwarm_MetadataReceived; bitSwarm.StatusChanged += BitSwarm_StatusChanged; - if (File.Exists(input.Text.Trim())) - bitSwarm.Initiliaze(input.Text.Trim()); - else - bitSwarm.Initiliaze(new Uri(input.Text.Trim())); - + bitSwarm.Open(input.Text); bitSwarm.Start(); } catch (Exception e1) @@ -106,7 +102,7 @@ private void BitSwarm_MetadataReceived(object source, BitSwarm.MetadataReceivedA torrent = e.Torrent; output.Text += bitSwarm.DumpTorrent().Replace("\n", "\r\n"); - for (int i = 0; i < torrent.data.files.Count; i++) + for (int i = 0; i < torrent.file.paths.Count; i++) listBox1.Items.Add(torrent.file.paths[i]); listBox1.BeginUpdate(); @@ -139,10 +135,10 @@ private void BitSwarm_StatusChanged(object source, BitSwarm.StatusChangedArgs e) { output.Text += "\r\n\r\nStopped at " + DateTime.Now.ToString("G", DateTimeFormatInfo.InvariantInfo); - if (e.Status == 0) + if (e.Status == 2) { output.Text += "\r\n\r\n" + "An error occurred :(\r\n\t" + e.ErrorMsg; - MessageBox.Show("An error occured :( " + e.ErrorMsg); + MessageBox.Show("An error occured :( \r\n" + e.ErrorMsg); } } @@ -164,15 +160,15 @@ private void BitSwarm_StatsUpdated(object source, BitSwarm.StatsUpdatedArgs e) etaAvg.Text = TimeSpan.FromSeconds(e.Stats.AvgETA).ToString(@"hh\:mm\:ss"); etaCur.Text = TimeSpan.FromSeconds(e.Stats.ETA).ToString(@"hh\:mm\:ss"); - bDownloaded.Text = Utils.BytesToReadableString(e.Stats.BytesDownloaded); + bDownloaded.Text = Utils.BytesToReadableString(e.Stats.BytesDownloaded + e.Stats.BytesDownloadedPrevSession); bDropped.Text = Utils.BytesToReadableString(e.Stats.BytesDropped); pPeers.Text = e.Stats.PeersTotal.ToString(); pInqueue.Text = e.Stats.PeersInQueue.ToString(); pConnecting.Text = e.Stats.PeersConnecting.ToString(); pConnected.Text = (e.Stats.PeersConnecting + e.Stats.PeersConnected).ToString(); - pFailed.Text = bitSwarm.dht.status == DHT.Status.RUNNING ? "On" : "Off"; - pFailed1.Text = bitSwarm.dhtPeers.ToString(); - pFailed2.Text = bitSwarm.trackersPeers.ToString(); + pFailed.Text = bitSwarm.isDHTRunning ? "On" : "Off"; + pFailed1.Text = e.Stats.DHTPeers.ToString(); + pFailed2.Text = e.Stats.TRKPeers.ToString(); pChoked.Text = e.Stats.PeersChoked.ToString(); pUnchocked.Text = e.Stats.PeersUnChoked.ToString(); pDownloading.Text = e.Stats.PeersDownloading.ToString(); diff --git a/BitSwarm (WinForms Demo)/packages.config b/BitSwarm (WinForms Demo)/packages.config index cd42c47..42742a1 100644 --- a/BitSwarm (WinForms Demo)/packages.config +++ b/BitSwarm (WinForms Demo)/packages.config @@ -1,5 +1,6 @@  + diff --git a/BitSwarm/BitField.cs b/BitSwarm/BEP/Bitfield.cs similarity index 93% rename from BitSwarm/BitField.cs rename to BitSwarm/BEP/Bitfield.cs index 7e52780..44e7eaf 100644 --- a/BitSwarm/BitField.cs +++ b/BitSwarm/BEP/Bitfield.cs @@ -1,15 +1,9 @@ using System; using System.Collections.Generic; -namespace SuRGeoNix -{ - /* TODO: - * - * 1. locker will be removed for speed (let Beggar do the lock) - * 2. replace GetBit/SetBit directly within the methods for better performance - */ - - public class BitField +namespace SuRGeoNix.BitSwarmLib.BEP +{ + public class Bitfield { public byte[] bitfield { get; private set; } public int size { get; private set; } @@ -17,13 +11,13 @@ public class BitField readonly object locker = new object(); - public BitField(int size) + public Bitfield(int size) { bitfield = new byte[((size-1)/8) + 1]; this.size = size; setsCounter = 0; } - public BitField(byte[] bitfield, int size) + public Bitfield(byte[] bitfield, int size) { if (size > (bitfield.Length * 8) || size <= ((bitfield.Length-1) * 8)) throw new Exception("Out of range"); @@ -67,7 +61,6 @@ public bool GetBit(int input) return true; } - // TODO: Review locking public int GetFirst0() { int bytePos = 0; @@ -107,7 +100,7 @@ public int GetFirst0(int from, int to = -1) return -1; } - public int GetFirst01(BitField bitfield) + public int GetFirst01(Bitfield bitfield) { if (bitfield == null) return -2; @@ -124,7 +117,7 @@ public int GetFirst01(BitField bitfield) return -1; } - public int GetFirst01(BitField bitfield, int from, int to = -1) + public int GetFirst01(Bitfield bitfield, int from, int to = -1) { to = to == -1 ? size - 1 : to; @@ -173,7 +166,7 @@ public int GetFirst0Reversed(int from = 0, int to = -1) return -1; } - public int GetFirst01Reversed(BitField bitfield, int from = 0, int to = -1) + public int GetFirst01Reversed(Bitfield bitfield, int from = 0, int to = -1) { to = to == -1 ? size - 1 : to; @@ -208,7 +201,6 @@ public List GetAll0(int from = 0, int to = -1) List ret = new List(); int cur = -1; - //int from = 0; while (from <= to && (cur = GetFirst0(from, to)) >= 0) { @@ -218,7 +210,7 @@ public List GetAll0(int from = 0, int to = -1) return ret; } - public List GetAll0(BitField bitfield, int from = 0, int to = -1) + public List GetAll0(Bitfield bitfield, int from = 0, int to = -1) { to = to == -1 ? size - 1 : to; @@ -326,16 +318,16 @@ public void SetAll() } } - public BitField Clone() + public Bitfield Clone() { - BitField clone = new BitField(size); + Bitfield clone = new Bitfield(size); Buffer.BlockCopy(bitfield, 0, clone.bitfield, 0, bitfield.Length); clone.setsCounter = setsCounter; return clone; } - public bool CopyFrom(BitField bitfield) + public bool CopyFrom(Bitfield bitfield) { lock (locker) { @@ -349,7 +341,7 @@ public bool CopyFrom(BitField bitfield) return true; } - public bool CopyFrom(BitField bitfield, int from, int to =-1) + public bool CopyFrom(Bitfield bitfield, int from, int to =-1) { to = to == -1 ? size - 1 : to; diff --git a/BitSwarm/DHT.cs b/BitSwarm/BEP/DHT.cs similarity index 99% rename from BitSwarm/DHT.cs rename to BitSwarm/BEP/DHT.cs index c35878a..ececa24 100644 --- a/BitSwarm/DHT.cs +++ b/BitSwarm/BEP/DHT.cs @@ -20,7 +20,7 @@ using BencodeNET.Objects; using BencodeNET.Parsing; -namespace SuRGeoNix.BEP +namespace SuRGeoNix.BitSwarmLib.BEP { public class DHT { @@ -454,7 +454,7 @@ private void Beggar() { Node node = bucketNodesPointer[curNodeKey]; - ThreadPool.QueueUserWorkItem(new WaitCallback(x => + System.Threading.ThreadPool.QueueUserWorkItem(new WaitCallback(x => { GetPeers(node); Interlocked.Decrement(ref curThreads); diff --git a/BitSwarm/Peer.cs b/BitSwarm/BEP/Peer.cs similarity index 98% rename from BitSwarm/Peer.cs rename to BitSwarm/BEP/Peer.cs index 2cf865b..fc88513 100644 --- a/BitSwarm/Peer.cs +++ b/BitSwarm/BEP/Peer.cs @@ -7,11 +7,9 @@ using BencodeNET.Parsing; using BencodeNET.Objects; -//using static SuRGeoNix.BitSwarm.BSTP; - -namespace SuRGeoNix.BEP +namespace SuRGeoNix.BitSwarmLib.BEP { - internal class Peer + class Peer { #region Declaration | Properties @@ -143,7 +141,7 @@ public class Stage { public string version; - public BitField bitfield; + public Bitfield bitfield; public Extensions extensions; //public bool handshake; @@ -320,7 +318,7 @@ public void Disconnect() #region Main Execution Flow (Connect -> Handshakes -> [ProcessMessages <-> Receive]) - public void Run(BitSwarm.BSTP bstp, int threadIndex) + public void Run(ThreadPool bstp, int threadIndex) { // CONNECT if (!Connect()) { Disconnect(); return; } @@ -416,9 +414,9 @@ private void ProcessMessage() if (stageYou.bitfield == null) { if (Beggar.torrent.data.pieces != 0) - stageYou.bitfield = new BitField(Beggar.torrent.data.pieces); + stageYou.bitfield = new Bitfield(Beggar.torrent.data.pieces); else - stageYou.bitfield = new BitField(15000); // MAX PIECES GUESS? + stageYou.bitfield = new Bitfield(15000); // MAX PIECES GUESS? } int havePiece = Utils.ToBigEndian(recvBuff); @@ -433,12 +431,14 @@ private void ProcessMessage() stageYou.haveNone = false; byte[] bitfield = new byte[recvBuff.Length]; Buffer.BlockCopy(recvBuff, 0, bitfield, 0, recvBuff.Length); - stageYou.bitfield = new BitField(bitfield, Beggar.torrent.data.pieces != 0 ? Beggar.torrent.data.pieces : recvBuff.Length * 8); + stageYou.bitfield = new Bitfield(bitfield, Beggar.torrent.data.pieces != 0 ? Beggar.torrent.data.pieces : recvBuff.Length * 8); return; case Messages.PIECE: if (Beggar.Options.Verbosity > 0) Log(2, "[MSG ] Piece"); + status = Status.DOWNLOADING; // Bug was noticed Downloading peer was in READY and couldn't get out with timeout + Receive(4); // [Piece Id] int piece = Utils.ToBigEndian(recvBuff); Receive(4); // [Offset] @@ -711,6 +711,11 @@ private void ProcessMessage() private void Receive(int len) { + /* TODO + * 1. Review modulo (heavy for cpu) + * 2. Review sleep time (it depends on how many blocks we request at the once / at the last block it should not sleep enough so it will re-request quickly) + */ + int curLoop = 0; long startedAt = 0; long diffMs; diff --git a/BitSwarm/BEP/Torrent.cs b/BitSwarm/BEP/Torrent.cs new file mode 100644 index 0000000..cd68c97 --- /dev/null +++ b/BitSwarm/BEP/Torrent.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Web; + +using BencodeNET.Parsing; +using BencodeNET.Objects; + +using SuRGeoNix.Partfiles; + +namespace SuRGeoNix.BitSwarmLib.BEP +{ + [Serializable] + public class Torrent : IDisposable + { + [NonSerialized] + internal BitSwarm bitSwarm; + [NonSerialized] + private static BencodeParser bParser = new BencodeParser(); + [NonSerialized] + public static SHA1 sha1 = new SHA1Managed(); + + public TorrentFile file; + public TorrentData data; + public MetaData metadata; + + [Serializable] + public struct TorrentFile + { + // SHA-1 of 'info' + public string infoHash { get; set; } + + // 'announce' | 'announce-list' + public List trackers { get; set; } + + // 'info' + + // 'name' | 'length' + public string name { get; set; } + public long length { get; set; } + + // ['path' | 'length'] + public List paths { get; set; } + public List lengths { get; set; } + + public int pieceLength { get; set; } + + public List pieces; + } + + [Serializable] + public struct TorrentData + { + public bool isDone { get; set; } + + [NonSerialized] + public Partfile[] files; + + public List filesIncludes { get; set; } + public string folder { get; set; } + public string folderTemp { get; set; } + public long totalSize { get; set; } + + public int pieces { get; set; } + public int pieceSize { get; set; } + public int pieceLastSize { get; set; } // NOTE: it can be 0, it should be equals with pieceSize in case of totalSize % pieceSize = 0 + + public int blocks { get; set; } + public int blockSize { get; set; } + public int blockLastSize { get; set; } + public int blocksLastPiece { get; set; } + + [NonSerialized] + public Bitfield progress; + [NonSerialized] + public Bitfield requests; + [NonSerialized] + public Bitfield progressPrev; + + [NonSerialized] + internal Dictionary pieceProgress; + + internal class PieceProgress + { + public PieceProgress(ref TorrentData data, int piece) + { + bool isLastPiece= piece == data.pieces - 1 && data.totalSize % data.pieceSize != 0; + + this.piece = piece; + this.data = !isLastPiece ? new byte[data.pieceSize] : new byte[data.pieceLastSize]; + this.progress = !isLastPiece ? new Bitfield(data.blocks): new Bitfield(data.blocksLastPiece); + this.requests = !isLastPiece ? new Bitfield(data.blocks): new Bitfield(data.blocksLastPiece); + } + public int piece; + public byte[] data; + public Bitfield progress; + public Bitfield requests; + } + } + + [Serializable] + public struct MetaData + { + public bool isDone { get; set; } + + [NonSerialized] + public Partfile file; + public int pieces { get; set; } + public long totalSize { get; set; } + + [NonSerialized] + public Bitfield progress; + + public int parallelRequests; + } + + public Torrent (BitSwarm bitSwarm) + { + this.bitSwarm = bitSwarm; + + file = new TorrentFile(); + data = new TorrentData(); + metadata = new MetaData(); + + file.trackers = new List(); + } + + public void FillFromMagnetLink(Uri magnetLink) + { + // TODO: Check v2 Magnet Link + // http://www.bittorrent.org/beps/bep_0009.html + + NameValueCollection nvc = HttpUtility.ParseQueryString(magnetLink.Query); + string[] xt = nvc.Get("xt") == null ? null : nvc.GetValues("xt")[0].Split(Char.Parse(":")); + if (xt == null || xt.Length != 3 || xt[1].ToLower() != "btih" || xt[2].Length < 20) throw new Exception("[Magnet][xt] No hash found " + magnetLink); + + file.name = nvc.Get("dn") == null ? null : nvc.GetValues("dn")[0] ; + file.length = nvc.Get("xl") == null ? 0 : (int) UInt32.Parse(nvc.GetValues("xl")[0]); + file.infoHash = xt[2]; + + // Base32 Hash + if (file.infoHash.Length != 40) + { + if (Regex.IsMatch(file.infoHash,@"[QAZ2WSX3EDC4RFV5TGB6YHN7UJM8K9LP]+")) + { + try + { + file.infoHash = Utils.ArrayToStringHex(Utils.FromBase32String(file.infoHash)); + if (file.infoHash.Length != 40) throw new Exception("[Magnet][xt] No valid hash found " + magnetLink); + } catch (Exception) { throw new Exception("[Magnet][xt] No valid hash found " + magnetLink); } + } else { throw new Exception("[Magnet][xt] No valid hash found " + magnetLink); } + } + + string[] tr = nvc.Get("tr") == null ? null : nvc.GetValues("tr"); + if (tr == null) return; + + for (int i=0; i(fileName); + BDictionary bInfo; + + if (bdicTorrent["info"] != null) + { + bInfo = (BDictionary) bdicTorrent["info"]; + FillTrackersFromInfo(bdicTorrent); + } + else if (bdicTorrent["name"] != null) + bInfo = bdicTorrent; + else + throw new Exception("Invalid torrent file"); + + file.infoHash = Utils.ArrayToStringHex(sha1.ComputeHash(bInfo.EncodeAsBytes())); + file.name = ((BString) bInfo["name"]).ToString(); + + return bInfo; + } + public void FillFromMetadata() + { + try + { + if (metadata.file == null) bitSwarm.StopWithError("No metadata found"); + + string curFilePath = Path.Combine(metadata.file.Options.Folder, metadata.file.Filename); + string curPath = (new FileInfo(curFilePath)).DirectoryName; + + metadata.file.Dispose(); + BDictionary bInfo = (BDictionary) bParser.Parse(curFilePath); + + if (file.infoHash != Utils.ArrayToStringHex(sha1.ComputeHash(bInfo.EncodeAsBytes()))) + bitSwarm.StopWithError("[CRITICAL] Metadata SHA1 validation failed"); + + file.name = ((BString) bInfo["name"]).ToString(); + + string torrentName = Utils.GetValidFileName(file.name) + ".torrent"; + + if (!File.Exists(Path.Combine(curPath, torrentName))) File.Move(curFilePath, Path.Combine(bitSwarm.Options.FolderTorrents, torrentName)); + + FillFromInfo(bInfo); + } catch (Exception e) { bitSwarm.StopWithError($"FillFromMetadata(): {e.Message} - {e.StackTrace}"); } + + } + public void FillTrackersFromTrackersPath(string fileName) + { + try + { + if (fileName == null || fileName.Trim() == "" || !File.Exists(fileName)) return; + + string[] trackers = File.ReadAllLines(fileName); + + foreach (var tracker in trackers) + try { file.trackers.Add(new Uri(tracker)); } catch (Exception) { } + } catch (Exception) { } + } + public void FillTrackersFromInfo(BDictionary torrent) + { + string tracker = null; + BList trackersBList = null; + + if (torrent["announce"] != null) + tracker = ((BString) torrent["announce"]).ToString(); + + if (torrent["announce-list"] != null) + trackersBList = (BList) torrent["announce-list"]; + + if (trackersBList != null) + for (int i=0; i(); + + Partfiles.Options opt = new Partfiles.Options(); + opt.AutoCreate = true; + + if (isMultiFile) + { + file.paths = GetPathsFromInfo(bInfo); + data.files = new Partfile[file.paths.Count]; + file.lengths = GetFileLengthsFromInfo(bInfo, out long tmpTotalSize); + data.totalSize = tmpTotalSize; + + data.folder = Path.Combine(bitSwarm.Options.FolderComplete , Utils.GetValidPathName(file.name)); + data.folderTemp = Path.Combine(bitSwarm.Options.FolderIncomplete, Utils.GetValidPathName(file.name)); + + if (Directory.Exists(data.folder)) bitSwarm.StopWithError($"Torrent folder already exists! {data.folder}"); + if (Directory.Exists(data.folderTemp)) Directory.Delete(data.folderTemp, true); + + opt.Folder = data.folder; + opt.PartFolder = data.folderTemp; + + for (int i=0; i() { file.name }; + file.lengths = new List() { file.length }; + + data.filesIncludes.Add(file.name); + } + + data.pieces = file.pieces.Count; + data.pieceSize = file.pieceLength; + data.pieceLastSize = (int) (data.totalSize % data.pieceSize); // NOTE: it can be 0, it should be equals with pieceSize in case of totalSize % pieceSize = 0 + + data.blockSize = Math.Min(Peer.MAX_DATA_SIZE, data.pieceSize); + data.blocks = ((data.pieceSize -1) / data.blockSize) + 1; + data.blockLastSize = data.pieceLastSize % data.blockSize == 0 ? data.blockSize : data.pieceLastSize % data.blockSize; + data.blocksLastPiece= ((data.pieceLastSize -1) / data.blockSize) + 1; + + data.progress = new Bitfield(data.pieces); + data.requests = new Bitfield(data.pieces); + data.progressPrev = new Bitfield(data.pieces); + data.pieceProgress = new Dictionary(); + + SaveSession(); + } + + public void FillFromSession() + { + data.files = new Partfile[file.paths == null ? 1 : file.paths.Count]; + data.filesIncludes = new List(); + + Partfiles.Options opt = new Partfiles.Options(); + opt.AutoCreate = true; + + if (data.folder != null) + { + data.folder = Path.Combine(bitSwarm.Options.FolderComplete , Utils.GetValidPathName(file.name)); + data.folderTemp = Path.Combine(bitSwarm.Options.FolderIncomplete, Utils.GetValidPathName(file.name)); + + opt.Folder = data.folder; + + for (int i=0; i(); + + long curSize = 0; + int curFile = 0; + int prevFile =-1; + long firstByte; + + for (int piece =0; piece GetPathsFromInfo(BDictionary info) + { + BList files = (BList) info["files"]; + if (files == null) return null; + + List fileNames = new List(); + + for (int i=0; i GetFileLengthsFromInfo(BDictionary info, out long totalSize) + { + totalSize = 0; + + BList files = (BList) info["files"]; + if (files == null) return null; + List lens = new List(); + + for (int i=0; i GetHashesFromInfo(BDictionary info) + { + byte[] hashBytes = ((BString) info["pieces"]).Value.ToArray(); + List hashes = new List(); + + for (int i=0; i MaxThreads- Running; - public int Running; - public int ShortRun => Running - LongRun; - public int LongRun; - - public ConcurrentStack peersForDispatch; - public BSTPThread[] Threads; - - private readonly object lockerThreads = new object(); - public void Initialize(int minThreads, int maxThreads, ConcurrentStack peersStack) - { - lock (lockerThreads) - { - //Console.WriteLine("Initializing... Threads -> " + (Threads == null ? "Null" : Threads.Length.ToString())); - Dispose(); - - Stop = false; - MinThreads = minThreads; - MaxThreads = maxThreads; - Running = maxThreads; - Threads = new BSTPThread[MaxThreads]; + public static string Version { get; private set; } = Regex.Replace(Assembly.GetExecutingAssembly().GetName().Version.ToString(), @"\.0$", ""); - peersForDispatch = peersStack; + #region Enums - for (int i=0; i { ThreadRun(cacheI); }); - Threads[i].thread.IsBackground = true; - Threads[i].thread.Start(); - } - private void ThreadRun(int index) - { - //Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Started"); - - Interlocked.Decrement(ref Running); - Threads[index].IsAlive = true; - - while (!Stop) - { - Threads[index].resetEvent.WaitOne(); - if (Stop) break; - - do - { - Threads[index].peer?.Run(this, index); - if (ShortRun > MinThreads || Stop || Threads == null || Threads[index] == null) break; - lock (peersForDispatch) - if (peersForDispatch.TryPop(out Peer tmp)) { Threads[index].peer = tmp; Threads[index].peer.status = Peer.Status.CONNECTING; } else break; - - } while (true); - - if (Threads != null && Threads[index] != null) Threads[index].IsRunning = false; - Interlocked.Decrement(ref Running); - } - - //Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Stopped"); - } - - public bool Dispatch(Peer peer) - { - lock (lockerThreads) - { - if (Stop || Running >= MaxThreads || ShortRun >= MinThreads) return false; - - foreach (var thread in Threads) - if (thread != null && !thread.IsRunning && thread.IsAlive) - { - if (Running >= MaxThreads || ShortRun >= MinThreads) return false; - - if (peer != null) peer.status = Peer.Status.CONNECTING; - thread.peer = peer; - thread.IsRunning= true; - Interlocked.Increment(ref Running); - thread.resetEvent.Set(); - - return true; - } - - return false; - } - } - public void Dispose() - { - lock (lockerThreads) - { - //Console.WriteLine("BSTP Disposing"); - - if (peersForDispatch != null) lock (peersForDispatch) peersForDispatch.Clear(); - Stop = true; - - if (Threads != null) - { - foreach (var thread in Threads) - { - //if (thread != null && thread.peer != null) - //{ - // if (thread.peer.status != Peer.Status.NEW && thread.peer.status != Peer.Status.FAILED1 && thread.peer.status != Peer.Status.FAILED2) Console.WriteLine($"BSTP!? Requests: {thread.peer.PiecesRequested}, Status {thread.peer.status.ToString()}" + (thread.peer.lastPieces != null ? ", Pieces: " + thread.peer.lastPieces.Count : "")); - // //thread.peer.Disconnect(); - //} - thread?.resetEvent.Set(); - } - //Console.WriteLine("BSTP Disconnects Done"); - int escape = 150; - while (Running > 0 && escape > 0) { Thread.Sleep(20); escape--; } - //if (escape <= 0) Console.WriteLine("BSTP Disposing Failed"); - } - - MinThreads = 0; - MaxThreads = 0; - Running = 0; - Threads = null; - - //Console.WriteLine("BSTP Disposed"); - } - } - - public class BSTPThread - { - public AutoResetEvent resetEvent = new AutoResetEvent(false); - public bool isLongRun { get; internal set; } - public bool IsRunning { get; internal set; } - public bool IsAlive { get; internal set; } - - public Thread thread; - public Peer peer; - } - } - #endregion - - - #region Focus Area [For Streaming] - internal Tuple focusArea; - internal Tuple lastFocusArea; - public Tuple FocusArea { get { lock (lockerTorrent) return focusArea; } set { lock (lockerTorrent) focusArea = value; } } - #endregion - - - #region Structs | Enums - public class DefaultOptions - { - public string DownloadPath { get; set; } = "."; - //public string TempPath { get; set; } - - public int MaxThreads { get; set; } = 150; // Max Total Connection Threads | Short-Run + Long-Run - public int MinThreads { get; set; } = 15; // Max New Connection Threads | Short-Run - - public int BoostThreads { get; set; } = 60; // Max New Connection Threads | Boot Boost - public int BoostTime { get; set; } = 30; // Boot Boost Time (Seconds) - - // -1: Auto | 0: Disabled | Auto will figure out SleepModeLimit from MaxRate - public int SleepModeLimit { get; set; } = 0; // Activates Sleep Mode (Low Resources) at the specify DownRate | DHT Stop, Re-Fills Stop (DHT/Trackers) & MinThreads Drop to MinThreads / 2 - - //public int DownloadLimit { get; set; } = -1; - //public int UploadLimit { get; set; } - - public int ConnectionTimeout { get; set; } = 600; - public int HandshakeTimeout { get; set; } = 800; - public int MetadataTimeout { get; set; } = 1600; - public int PieceTimeout { get; set; } = 5000; - public int PieceRetries { get; set; } = 0; - - public bool EnablePEX { get; set; } = true; - public bool EnableDHT { get; set; } = true; - public bool EnableTrackers { get; set; } = true; - public int PeersFromTracker { get; set; } = -1; - - public int BlockRequests { get; set; } = 6; - - public int Verbosity { get; set; } = 0; // 1 -> BitSwarm | DHT, 2 -> SavePiece | Peers | Trackers - public bool LogTracker { get; set; } = false; - public bool LogPeer { get; set; } = false; - public bool LogDHT { get; set; } = false; - public bool LogStats { get; set; } = false; - - public string TrackersPath { get; set; } = null; - } - public struct StatsStructure + public enum InputType { - public int DownRate { get; set; } - public int AvgRate { get; set; } - public int MaxRate { get; set; } - - public int AvgETA { get; set; } - public int ETA { get; set; } - - public int Progress { get; set; } - public int PiecesIncluded { get; set; } - public long BytesIncluded { get; set; } - public long BytesCurDownloaded { get; set; } - - public long BytesDownloaded { get; set; } - public long BytesDownloadedPrev { get; set; } - public long BytesUploaded { get; set; } - public long BytesDropped { get; set; } - - public int PeersTotal { get; set; } - public int PeersInQueue { get; set; } - public int PeersConnecting { get; set; } - public int PeersConnected { get; set; } - public int PeersFailed1 { get; set; } - public int PeersFailed2 { get; set; } - public int PeersFailed { get; set; } - public int PeersChoked { get; set; } - public int PeersUnChoked { get; set; } - public int PeersDownloading { get; set; } - public int PeersDropped { get; set; } - - public long StartTime { get; set; } - public long CurrentTime { get; set; } - public long EndTime { get; set; } - - public bool SleepMode { get; set; } - public bool BoostMode { get; set; } - public bool EndGameMode { get; set; } - - public int AlreadyReceived { get; set; } - public int Rejects; - - public int ConnectTimeouts; // Not used - public int HandshakeTimeouts; - public int PieceTimeouts; + Torrent, + Magnet, + Sha1, + Base32, + Session, + Unkown } - - public static string Version { get; private set; } = System.Text.RegularExpressions.Regex.Replace(Assembly.GetExecutingAssembly().GetName().Version.ToString(), @"\.0$", ""); - - public enum SleepModeState + private enum SleepModeState { Automatic, Manual, @@ -325,20 +93,26 @@ public StatusChangedArgs(int status, string errorMsg = "") public delegate void StatsUpdatedHandler(object source, StatsUpdatedArgs e); public class StatsUpdatedArgs : EventArgs { - public StatsStructure Stats { get; set; } - public StatsUpdatedArgs(StatsStructure stats) + public Stats Stats { get; set; } + public StatsUpdatedArgs(Stats stats) { Stats = stats; } } #endregion - #region Properties - public DefaultOptions Options; - public StatsStructure Stats; + #region Public Properties Exposed + public Options Options; + public Stats Stats; public bool isRunning => status == Status.RUNNING; public bool isPaused => status == Status.PAUSED; public bool isStopped => status == Status.STOPPED; + public bool isDHTRunning => dht.status == DHT.Status.RUNNING; + public string LoadedSessionFile { get; set; } + public Tuple FocusArea { get { lock (lockerTorrent) return focusArea; } set { lock (lockerTorrent) focusArea = value; } } + + internal Tuple focusArea; + internal Tuple lastFocusArea; #endregion #region Declaration @@ -347,13 +121,13 @@ public StatsUpdatedArgs(StatsStructure stats) readonly object lockerMetadata = new object(); // Generators (Hash / Random) - public static SHA1 sha1 = new SHA1Managed(); + private static SHA1 sha1 = new SHA1Managed(); private static Random rnd = new Random(); internal byte[] peerID; // Main [Torrent / Trackers / Peers / Options] - BSTP bstp; + ThreadPool bstp; internal ConcurrentDictionary peersStored {get; private set; } internal ConcurrentStack peersForDispatch {get; private set; } @@ -362,7 +136,7 @@ public StatsUpdatedArgs(StatsStructure stats) private List trackers; private Tracker.Options trackerOpt; - public DHT dht; + private DHT dht; private DHT.Options dhtOpt; internal Logger log; @@ -372,38 +146,31 @@ public StatsUpdatedArgs(StatsStructure stats) private long metadataLastRequested; private bool wasPaused; - - // More Stats private int curSecond = 0; private int prevSecond = 0; internal long prevStatsTicks = 0; - private int sha1Fails = 0; - public int dhtPeers = 0; // Not accurate (based on uniqueness) - public int trackersPeers = 0; // Not accurate (based on uniqueness) - public int pexPeers = 0; // Not accurate (based on uniqueness) #endregion - - #region Constructors / Initializers / Setup / IncludeFiles - public BitSwarm(DefaultOptions opt = null) { Options = (opt == null) ? new DefaultOptions() : opt; } - public void Initiliaze(string torrent) - { - Initiliaze(); - this.torrent.FillFromTorrentFile(torrent); - Setup(); - } - public void Initiliaze(Uri magnetLink) - { - Initiliaze(); - torrent.FillFromMagnetLink(magnetLink); - Setup(); - } + #region Initiliaze | Setup private void Initiliaze() { - if (Options.DownloadPath.Trim() == ".") Options.DownloadPath = Environment.CurrentDirectory; - if (!Directory.Exists(Options.DownloadPath)) Directory.CreateDirectory(Options.DownloadPath); + // https://github.com/dotnet/runtime/issues/25792 ? + + Options.FolderComplete = Environment.ExpandEnvironmentVariables(Options.FolderComplete); + Options.FolderIncomplete = Environment.ExpandEnvironmentVariables(Options.FolderIncomplete); + Options.FolderTorrents = Environment.ExpandEnvironmentVariables(Options.FolderTorrents); + Options.FolderSessions = Environment.ExpandEnvironmentVariables(Options.FolderSessions); - bstp = new BSTP(); + if (Options.TrackersPath != null) + Options.TrackersPath = Environment.ExpandEnvironmentVariables(Options.TrackersPath); + + if (!Directory.Exists(Options.FolderComplete)) Directory.CreateDirectory(Options.FolderComplete); + if (!Directory.Exists(Options.FolderIncomplete))Directory.CreateDirectory(Options.FolderIncomplete); + if (!Directory.Exists(Options.FolderTorrents)) Directory.CreateDirectory(Options.FolderTorrents); + if (!Directory.Exists(Options.FolderSessions)) Directory.CreateDirectory(Options.FolderSessions); + + bstp = new ThreadPool(); + Stats = new Stats(); status = Status.STOPPED; peerID = new byte[20]; rnd.NextBytes(peerID); @@ -411,16 +178,16 @@ private void Initiliaze() peersForDispatch = new ConcurrentStack(); trackers = new List(); - torrent = new Torrent(Options.DownloadPath); - torrent.metadata.progress = new BitField(20); // Max Metadata Pieces + torrent = new Torrent(this); + torrent.metadata.progress = new Bitfield(20); // Max Metadata Pieces torrent.metadata.pieces = 2; // Consider 2 Pieces Until The First Response torrent.metadata.parallelRequests=14; // How Many Peers We Will Ask In Parallel (firstPieceTries/2) if (Options.Verbosity > 0) { - log = new Logger(Path.Combine(Options.DownloadPath, "Logs", "session" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log" ), true); + log = new Logger(Path.Combine(Options.FolderComplete, "Logs", "session" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log" ), true); if (Options.EnableDHT && Options.LogDHT) - logDHT = new Logger(Path.Combine(Options.DownloadPath, "Logs", "session" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + "_DHT.log"), true); + logDHT = new Logger(Path.Combine(Options.FolderComplete, "Logs", "session" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + "_DHT.log"), true); } } private void Setup() @@ -472,30 +239,159 @@ private void Setup() MetadataReceived?.Invoke(this, new MetadataReceivedArgs(torrent)); } } - - private void FillTrackersFromTorrent() + #endregion + + #region Public Methods Exposed [Ctor | Open | Start | Pause | Dispose | IncludeFiles | Dumps] + /// + /// Creates a new instance of BitSwarm (subscribe to events before Open/Start) + /// + /// BitSwarm's main options (can be changed later) + public BitSwarm(Options opt = null) { Options = (opt == null) ? new Options() : opt; } + + /// + /// Opens a Torrent File, Magnet Link, SHA-1 Hex Hash, Base32 Hash or a previously Saved Session (.bsf) + /// + /// Torrent File, Magnet Link, SHA-1 Hex Hash, Base32 Hash or Saved Session File + public void Open(string input) { - foreach (Uri uri in torrent.file.trackers) + if (input == null || input.Trim() == "") throw new Exception("No input has been provided"); + input = input.Trim(); + + Initiliaze(); + + bool isSessionFile = false; + //bool isTorrentFile = false; + BDictionary bInfo = null; + + // Check Torrent | Session + if (File.Exists(input)) { - if (uri.Scheme.ToLower() == "http" || uri.Scheme.ToLower() == "https" || uri.Scheme.ToLower() == "udp") + FileInfo fi = new FileInfo(input); + + if (fi.Extension.ToLower() == ".bsf") + isSessionFile = true; + else if (fi.Extension.ToLower() == ".torrent") + bInfo = torrent.FillFromTorrentFile(input); + else + throw new Exception("Unknown file format has been provided"); + } + + // Check MagnetLink | SHA-1 Hash | Base32 + else + { + if (Regex.IsMatch(input, @"^magnet:", RegexOptions.IgnoreCase)) + torrent.FillFromMagnetLink(new Uri(input)); + else if (Regex.IsMatch(input, @"^[0-9abcdef]+$", RegexOptions.IgnoreCase)) { - bool found = false; - foreach (var tracker in trackers) - if (tracker.uri.Scheme == uri.Scheme && tracker.uri.DnsSafeHost == uri.DnsSafeHost && tracker.uri.Port == uri.Port) { found = true; break; } - if (found) continue; + torrent.file.infoHash = input; + if (torrent.file.infoHash.Length != 40) throw new Exception("[SHA-1] No valid hash found"); - if (Options.LogTracker) Log($"[Torrent] [Tracker] [ADD] {uri}"); - trackers.Add(new Tracker(uri, trackerOpt)); + } + else if (Regex.IsMatch(input, @"^[a-z2-7]+$", RegexOptions.IgnoreCase)) + { + torrent.file.infoHash = Utils.ArrayToStringHex(Utils.FromBase32String(input)); + if (torrent.file.infoHash.Length != 40) throw new Exception("[BASE32] No valid hash found"); } else - if (Options.LogTracker) Log($"[Torrent] [Tracker] [ADD] {uri} Protocol not implemented"); + throw new Exception("Unknown string input has been provided"); + } + + string sessionFilePath = Path.Combine(Options.FolderSessions, torrent.file.infoHash.ToUpper() + ".bsf"); // string.Join("_", torrent.file.name.Split(Path.GetInvalidFileNameChars())); + + // Input file is Session or a saved Session found in temp (for provided hash) + if (isSessionFile || File.Exists(sessionFilePath)) + { + LoadedSessionFile = isSessionFile ? input : sessionFilePath; + torrent = Torrent.LoadSession(LoadedSessionFile); + if (torrent == null) throw new Exception($"Unable to load session file {LoadedSessionFile}"); + torrent.bitSwarm = this; + torrent.FillFromSession(); } + + // Input file is Torrent + else if (bInfo != null) + torrent.FillFromInfo(bInfo); + + Setup(); + } + /// + /// Starts BitSwarm + /// + public void Start() + { + if (status == Status.RUNNING || (torrent.data.progress != null && torrent.data.progress.GetFirst0() == - 1)) return; + + wasPaused = status == Status.PAUSED; + status = Status.RUNNING; + Stats.EndGameMode = false; + torrent.data.isDone = false; + + Utils.EnsureThreadDoneNoAbort(beggar); + + beggar = new Thread(() => + { + Beggar(); + + if (torrent.data.isDone) + StatusChanged?.Invoke(this, new StatusChangedArgs(0)); + else + StatusChanged?.Invoke(this, new StatusChangedArgs(1)); + }); + + beggar.IsBackground = true; + beggar.Start(); + } + /// + /// Pauses BitSwarm + /// + public void Pause() + { + if (status == Status.PAUSED) return; + + status = Status.PAUSED; + Utils.EnsureThreadDoneNoAbort(beggar, 1500); + bstp.Dispose(); } + /// + /// Stops BitSwarm & Disposes + /// + /// To avoid waiting for proper disposing + public void Dispose(bool force = false) + { + /* TODO + * In case of Stop and not Abort we could do an end game in the current working pieces to avoid loosing those bytes + * Another solution would be to ensure proper dispose/close and serialize also working pieces (and load them back to session) | more possibilities for corrupted data but will be validated in SHA-1 validation + */ + + try + { + status = Status.STOPPED; + + if (!force) + Utils.EnsureThreadDoneNoAbort(beggar, 1500); + else + bstp.Stop = true; + + lock (lockerTorrent) + { + if (torrent != null) torrent.Dispose(); + if (logDHT != null) logDHT. Dispose(); + if (log != null) log. Dispose(); + bstp.Threads = null; + peersStored?.Clear(); + peersForDispatch?.Clear(); + } + } catch (Exception) { } + } + /// + /// Forces BitSwarm to download only the specified files (can be called at any time) + /// + /// Downloads only the specified files (from torrent.file.paths) public void IncludeFiles(List includeFiles) { if (!torrent.metadata.isDone) return; - BitField newProgress = new BitField(torrent.data.pieces); + Bitfield newProgress = new Bitfield(torrent.data.pieces); newProgress.SetAll(); Stats.PiecesIncluded = 0; @@ -540,7 +436,7 @@ public void IncludeFiles(List includeFiles) Stats.PiecesIncluded += (int) ((Stats.BytesIncluded/torrent.file.pieceLength) + 1); } } - public void CloneProgress() + internal void CloneProgress() { torrent.data.requests = torrent.data.progress.Clone(); @@ -549,141 +445,175 @@ public void CloneProgress() pieceBlocks.requests = pieceBlocks.progress.Clone(); } - #endregion - - - #region Start / Pause / Dispose - public void Start() + /// + /// Checks if the provided input could be handled by BitSwarm + /// + /// Torrent File, Magnet Link, SHA-1 Hex Hash, Base32 Hash or Saved Session File + /// Identified InputType or InputType.Unknown + public static InputType ValidateInput(string input) { - if (status == Status.RUNNING || (torrent.data.progress != null && torrent.data.progress.GetFirst0() == - 1)) return; - - wasPaused = status == Status.PAUSED; - status = Status.RUNNING; - Stats.EndGameMode = false; - torrent.data.isDone = false; + if (input == null || input.Trim() == "") return InputType.Unkown; - Utils.EnsureThreadDoneNoAbort(beggar); + input = input.Trim(); - beggar = new Thread(() => + if (File.Exists(input)) { - Beggar(); + FileInfo fi = new FileInfo(input); + if (fi.Extension.ToLower() == ".bsf") return InputType.Session; + if (fi.Extension.ToLower() == ".torrent") return InputType.Torrent; - if (torrent.data.isDone) - StatusChanged?.Invoke(this, new StatusChangedArgs(0)); - else - StatusChanged?.Invoke(this, new StatusChangedArgs(1)); - }); + return InputType.Unkown; + } - beggar.IsBackground = true; - beggar.Start(); - } - public void Pause() - { - if (status == Status.PAUSED) return; + if (Regex.IsMatch(input, @"^magnet:", RegexOptions.IgnoreCase)) return InputType.Magnet; + if (Regex.IsMatch(input, @"^[0-9abcdef]+$", RegexOptions.IgnoreCase) && input.Length == 40) return InputType.Sha1; + if (Regex.IsMatch(input, @"^[a-z2-7]+$", RegexOptions.IgnoreCase) && Utils.ArrayToStringHex(Utils.FromBase32String(input)).Length == 40) return InputType.Base32; - status = Status.PAUSED; - Utils.EnsureThreadDoneNoAbort(beggar, 1500); - bstp.Dispose(); + return InputType.Unkown; } - public void Dispose(bool force = false) + + public string DumpPeers(int sortBy = 2) { - try - { - status = Status.STOPPED; + Dictionary peersDump = new Dictionary(); + lock (peersForDispatch) + foreach (var thread in bstp.Threads) + { + if (!thread.isLongRun || thread.peer == null) continue; - if (!force) - Utils.EnsureThreadDoneNoAbort(beggar, 1500); - else - bstp.Stop = true; + Peer peer = thread.peer; - lock (lockerTorrent) - { - if (torrent != null) torrent.Dispose(); - if (logDHT != null) logDHT. Dispose(); - if (log != null) log. Dispose(); - bstp.Threads = null; - peersStored?.Clear(); - peersForDispatch?.Clear(); + if (peer.status == Peer.Status.DOWNLOADING) + { + string verA = ""; + string verB = ""; + + if (peer.stageYou != null && peer.stageYou.version != null) + { + int vPos = peer.stageYou.version.LastIndexOf('/'); + if (vPos == -1) vPos = peer.stageYou.version.LastIndexOf(' '); + + verA = vPos == -1 ? peer.stageYou.version : peer.stageYou.version.Substring(0, vPos); + verB = vPos == -1 ? "" : peer.stageYou.version.Substring(vPos + 1); + + if (verA.Length > 25) verA = verA.Substring(0, 25); + if (verB.Length > 11) verA = verA.Substring(0, 11); + } + + long downRate = peer.GetDownRate()/1024; + string peerDump = $"[{peer.host}".PadRight(40, ' ') + ":" + PadStats($"{peer.port}]", 6) + " " + + $"[{verA}".PadRight(26, ' ') + PadStats($"{verB}]", 12) + " " + + $"[{String.Format("{0:n0}", downRate).PadLeft(6, ' ')} KB/s]"; + + //if (sortBy == 2) + peersDump.Add(peerDump, downRate.ToString()); + } } - } catch (Exception) { } - } - #endregion - #region Feeders [Torrent -> Trackers | Trackers -> Peers | DHT -> Peers | Client -> Stats] - private void StartTrackers() + string dump = ""; + //if (sortBy == 2) + foreach (var peerDump in peersDump.OrderBy(x => int.Parse(x.Value))) + dump += peerDump.Key + " \r\n"; + + + dump += "\r\n" + ($"[{Stats.PeersDownloading}/{Stats.PeersChoked} D/W] [{String.Format("{0:n0}", Stats.DownRate/1024).PadLeft(6, ' ')} KB/s]").PadLeft(100, ' ') + " \r\n"; + + return dump; + } + public string DumpTorrent() { - for (int i=0; i newPeers, PeersStorage storage) + public string DumpStats() { - int countNew = 0; + string mode = "NRM"; + if (Stats.SleepMode) + mode = "SLP"; + else if (Stats.BoostMode) + mode = "BST"; + else if (Stats.EndGameMode) + mode = "END"; - lock (peersForDispatch) - foreach (KeyValuePair peerKV in newPeers) - { - if (!peersStored.ContainsKey(peerKV.Key) && !peersBanned.Contains(peerKV.Key)) - { - peersStored.TryAdd(peerKV.Key, peerKV.Value); - Peer peer = new Peer(peerKV.Key, peerKV.Value); - peersForDispatch.Push(peer); //if (!bstp.Dispatch(peer)) peersForDispatch.Push(peer); - countNew++; - } - } + int alignCenter = torrent.file.name.Length + 2 + ((100 - torrent.file.name.Length) / 2); + string stats = $"{("[" + torrent.file.name + "]").PadLeft(alignCenter,'=').PadRight(100, '=')}\n"; + + string includedBytes = $" ({Utils.BytesToReadableString(Stats.BytesCurDownloaded)} / {Utils.BytesToReadableString(Stats.PiecesIncluded * torrent.data.pieceSize)})"; // Not the actual file sizes but pieces size (- first/last chunks) + stats += "\n"; + stats += $"BitSwarm " + + $"{PadStats(TimeSpan.FromSeconds(curSecond).ToString(@"hh\:mm\:ss"), 15)} | " + + $"{PadStats("ETA " + TimeSpan.FromSeconds(Stats.AvgETA).ToString(@"hh\:mm\:ss"), 20)} | " + + $"{Utils.BytesToReadableString(Stats.BytesDownloaded + Stats.BytesDownloadedPrevSession)} / {Utils.BytesToReadableString(torrent.data.totalSize)}{(Stats.PiecesIncluded == torrent.data.pieces ? "" : includedBytes)} "; + - if (peersForDispatch.Count > 0 && bstp.ShortRun < bstp.MinThreads) - { - for (int i=0; i< Math.Min(peersForDispatch.Count, bstp.MinThreads); i++) - bstp.Dispatch(null); - } + stats += "\n"; + stats += $"v{Version}".PadRight(9, ' ') + + $"{PadStats(String.Format("{0:n0}", Stats.DownRate/1024), 11)} KB/s | " + + $"{PadStats(String.Format("{0:n1}", ((Stats.DownRate * 8)/1000.0)/1000.0), 15)} Mbps | " + + $"Max: {String.Format("{0:n0}", Stats.MaxRate/1024)} KB/s, {String.Format("{0:n0}", ((Stats.MaxRate * 8)/1000.0)/1000.0)} Mbps "; - if (storage == PeersStorage.TRACKER) - trackersPeers += countNew; - else if (storage == PeersStorage.DHT) - dhtPeers += countNew; - else if (storage == PeersStorage.PEX) - pexPeers += countNew; + stats += "\n"; + stats += $" " + + $"{PadStats($"Mode: {mode}", 15)} | " + + $"{PadStats(" ", 20)} | " + + $"Avg: {String.Format("{0:n0}", Stats.AvgRate/1024)} KB/s, {String.Format("{0:n0}", ((Stats.AvgRate * 8)/1000.0)/1000.0)} Mbps "; - if (Options.Verbosity > 0 && countNew > 0) Log($"[{storage.ToString()}] {countNew} Adding Peers"); - } - internal void ReFillPeers() - { - lock (peersForDispatch) + int progressLen = Stats.Progress.ToString().Length; + stats += "\n"; + for (int i=0; i<100; i++) { - if (peersForDispatch.Count > 0) - Console.WriteLine("Peers queue not empty! -> " + peersStored.Count); + if (i == 50 - progressLen) { stats += Stats.Progress + "%"; i += progressLen + 1; } + stats += i < Stats.Progress ? "|" : "-"; + } - peersForDispatch.Clear(); + stats += "\n"; + stats += $"[PEERS ] " + + $"{PadStats($"{Stats.PeersDownloading}/{Stats.PeersChoked}", 11)} D/W | " + + $"{PadStats($"{Stats.PeersConnecting}/{Stats.PeersInQueue}/{Stats.PeersTotal}", 14)} C/Q/T | " + + $"{Stats.PEXPeers}/{Stats.TRKPeers}/{Stats.DHTPeers} PEX/TRK/DHT {(dht.status == DHT.Status.RUNNING ? "(On)" : "(Off)")} "; - HashSet peersRunning = new HashSet(); + stats += "\n"; + stats += $"[PIECES] " + + $"{PadStats($"{torrent.data.progress.setsCounter}/{torrent.data.pieces}", 11)} D/T | " + + $"{PadStats($"{torrent.data.requests.setsCounter}", 14)} REQ | " + + $"{Utils.BytesToReadableString(Stats.BytesDropped)} / {Stats.AlreadyReceived} BLK (Drops) "; - if (bstp.Threads != null) - foreach (var thread in bstp.Threads) - if (thread.thread != null && thread.peer != null) peersRunning.Add(thread.peer.host); - foreach (var peerKV in peersStored) - { - if (!peersRunning.Contains(peerKV.Key) && !peersBanned.Contains(peerKV.Key)) - { - Peer peer = new Peer(peerKV.Key, peerKV.Value); - peersForDispatch.Push(peer); - } - } + stats += "\n"; + stats += $"[ERRORS] " + + $"{PadStats($"{Stats.PieceTimeouts}", 11)} TMO | " + + $"{PadStats($"{Stats.Rejects}", 14)} RJS | " + + $"{Stats.SHA1Failures} SHA "; - if (peersForDispatch.Count > 0 && bstp.ShortRun < bstp.MinThreads) - { - for (int i=0; i< Math.Min(peersForDispatch.Count, bstp.MinThreads); i++) - bstp.Dispatch(null); - } + stats += "\n"; + for (int i=0; i<100; i++) stats += "="; + stats += "\n"; - } + return stats; } - - private void FillStats() + static string PadStats(string str, int num) { return str.PadLeft(num, ' '); } + #endregion + + #region Feeders [Torrent -> Trackers | Trackers -> Peers | DHT -> Peers | Client -> Stats] + private void FillStats() { try { @@ -771,144 +701,95 @@ private void FillStats() } catch (Exception e) { Log($"[CRITICAL ERROR] Msg: {e.Message}\r\n{e.StackTrace}"); } } - public string DumpTorrent() + private void FillTrackersFromTorrent() { - if (torrent == null || !torrent.metadata.isDone) return "Metadata not ready"; - - string str = ""; - str += "=================\n"; - str += "|Torrent Details|\n"; - str += "=================\n"; - str += $"Pieces/Blocks: {torrent.data.pieces}/{torrent.data.blocks} | Piece Size: {torrent.data.pieceSize}/{torrent.data.totalSize % torrent.data.pieceSize} | Block Size: {torrent.data.blockSize}/{torrent.data.blockLastSize}\n"; - str += "\n"; - str += torrent.file.name + " (" + Utils.BytesToReadableString(torrent.data.totalSize) + ")\n"; - str += "\n"; - str += "-------\n"; - str += "|Files|\n"; - str += "-------\n"; - - str += $"- {Options.DownloadPath}\n"; - - for (int i=0; i newPeers, PeersStorage storage) { - Dictionary peersDump = new Dictionary(); + int countNew = 0; lock (peersForDispatch) - foreach (var thread in bstp.Threads) + foreach (KeyValuePair peerKV in newPeers) { - if (!thread.isLongRun || thread.peer == null) continue; + if (!peersStored.ContainsKey(peerKV.Key) && !peersBanned.Contains(peerKV.Key)) + { + peersStored.TryAdd(peerKV.Key, peerKV.Value); + Peer peer = new Peer(peerKV.Key, peerKV.Value); + peersForDispatch.Push(peer); //if (!bstp.Dispatch(peer)) peersForDispatch.Push(peer); + countNew++; + } + } - Peer peer = thread.peer; + if (peersForDispatch.Count > 0 && bstp.ShortRun < bstp.MinThreads) + { + for (int i=0; i< Math.Min(peersForDispatch.Count, bstp.MinThreads); i++) + bstp.Dispatch(null); + } - if (peer.status == Peer.Status.DOWNLOADING) - { - string verA = ""; - string verB = ""; + if (storage == PeersStorage.TRACKER) + Stats.TRKPeers += countNew; + else if (storage == PeersStorage.DHT) + Stats.DHTPeers += countNew; + else if (storage == PeersStorage.PEX) + Stats.PEXPeers += countNew; - if (peer.stageYou != null && peer.stageYou.version != null) - { - int vPos = peer.stageYou.version.LastIndexOf('/'); - if (vPos == -1) vPos = peer.stageYou.version.LastIndexOf(' '); + if (Options.Verbosity > 0 && countNew > 0) Log($"[{storage.ToString()}] {countNew} Adding Peers"); + } + internal void ReFillPeers() + { + lock (peersForDispatch) + { + if (peersForDispatch.Count > 0) + Console.WriteLine("Peers queue not empty! -> " + peersStored.Count); - verA = vPos == -1 ? peer.stageYou.version : peer.stageYou.version.Substring(0, vPos); - verB = vPos == -1 ? "" : peer.stageYou.version.Substring(vPos + 1); + peersForDispatch.Clear(); - if (verA.Length > 25) verA = verA.Substring(0, 25); - if (verB.Length > 11) verA = verA.Substring(0, 11); - } + HashSet peersRunning = new HashSet(); - long downRate = peer.GetDownRate()/1024; - string peerDump = $"[{peer.host}".PadRight(40, ' ') + ":" + PadStats($"{peer.port}]", 6) + " " + - $"[{verA}".PadRight(26, ' ') + PadStats($"{verB}]", 12) + " " + - $"[{String.Format("{0:n0}", downRate).PadLeft(6, ' ')} KB/s]"; + if (bstp.Threads != null) + foreach (var thread in bstp.Threads) + if (thread.thread != null && thread.peer != null) peersRunning.Add(thread.peer.host); - //if (sortBy == 2) - peersDump.Add(peerDump, downRate.ToString()); + foreach (var peerKV in peersStored) + { + if (!peersRunning.Contains(peerKV.Key) && !peersBanned.Contains(peerKV.Key)) + { + Peer peer = new Peer(peerKV.Key, peerKV.Value); + peersForDispatch.Push(peer); } } - string dump = ""; - //if (sortBy == 2) - foreach (var peerDump in peersDump.OrderBy(x => int.Parse(x.Value))) - dump += peerDump.Key + " \r\n"; - - - dump += "\r\n" + ($"[{Stats.PeersDownloading}/{Stats.PeersChoked} D/W] [{String.Format("{0:n0}", Stats.DownRate/1024).PadLeft(6, ' ')} KB/s]").PadLeft(100, ' ') + " \r\n"; + if (peersForDispatch.Count > 0 && bstp.ShortRun < bstp.MinThreads) + { + for (int i=0; i< Math.Min(peersForDispatch.Count, bstp.MinThreads); i++) + bstp.Dispatch(null); + } - return dump; + } } - static string PadStats(string str, int num) { return str.PadLeft(num, ' '); } #endregion @@ -1147,7 +1028,7 @@ internal void RequestPiece(Peer peer, bool imBeggar = false) } if (torrent.metadata.totalSize == 0) - { + { torrent.metadata.parallelRequests -= 2; peer.RequestMetadata(0, 1); Log($"[{peer.host.PadRight(15, ' ')}] [REQ ][M]\tPiece: 0, 1"); @@ -1379,12 +1260,18 @@ internal void MetadataPieceReceived(byte[] data, int piece, int offset, int tota if (torrent.metadata.totalSize == 0) { torrent.metadata.totalSize = totalSize; - torrent.metadata.progress = new BitField((totalSize/Peer.MAX_DATA_SIZE) + 1); + torrent.metadata.progress = new Bitfield((totalSize/Peer.MAX_DATA_SIZE) + 1); torrent.metadata.pieces = (totalSize/Peer.MAX_DATA_SIZE) + 1; - if (torrent.file.name != null) - torrent.metadata.file = new PartFile(Utils.FindNextAvailablePartFile(Path.Combine(Options.DownloadPath, string.Join("_", torrent.file.name.Split(Path.GetInvalidFileNameChars())) + ".torrent")), Peer.MAX_DATA_SIZE, totalSize, false); - else - torrent.metadata.file = new PartFile(Utils.FindNextAvailablePartFile(Path.Combine(Options.DownloadPath, "metadata" + rnd.Next(10000) + ".torrent")), Peer.MAX_DATA_SIZE, totalSize, false); + + Partfiles.Options opt = new Partfiles.Options(); + opt.Folder = Options.FolderTorrents; + opt.PartFolder = Options.FolderTorrents; + opt.AutoCreate = false; + opt.Overwrite = true; + opt.PartOverwrite = true; + + string metadataFileName = torrent.file.name == null ? torrent.file.infoHash + ".torrent" : Utils.GetValidFileName(torrent.file.name) + ".torrent"; + torrent.metadata.file = new Partfile(metadataFileName, Peer.MAX_DATA_SIZE, totalSize, opt); } if (torrent.metadata.progress.GetBit(piece)) { Log($"[{peer.host.PadRight(15, ' ')}] [RECV][M]\tPiece: {piece} Already received"); return; } @@ -1403,9 +1290,8 @@ internal void MetadataPieceReceived(byte[] data, int piece, int offset, int tota { // TODO: Validate Torrent's SHA-1 Hash with Metadata Info torrent.metadata.parallelRequests = -1000; - Log($"Creating Metadata File {torrent.metadata.file.FileName}"); - torrent.metadata.file.CreateFile(); - torrent.metadata.file.Dispose(); + Log($"Creating Metadata File {torrent.metadata.file.Filename}"); + torrent.metadata.file.Create(); torrent.FillFromMetadata(); torrent.metadata.isDone = true; Stats.PiecesIncluded = torrent.data.pieces; @@ -1503,7 +1389,7 @@ internal void PieceReceived(byte[] data, int piece, int offset, Peer peer) Stats.BytesDropped += torrent.data.pieceProgress[piece].data.Length; Stats.BytesDownloaded -= torrent.data.pieceProgress[piece].data.Length; - sha1Fails++; + Stats.SHA1Failures++; UnSetRequestsBit(piece); torrent.data.pieceProgress.Remove(piece); @@ -1608,13 +1494,16 @@ private void SavePiece(byte[] data, int piece, int size) long firstByte = (long)piece * torrent.file.pieceLength; long ends = firstByte + size; long sizeLeft = size; - int writePos = 0; + int writePos = 0; long curSize = 0; for (int i=0; i= lastFocusArea.Item1 && piece <= lastFocusArea.Item2) @@ -1689,7 +1577,7 @@ private void BanPeer(string host) } } } - class SHA1FailedPiece + private class SHA1FailedPiece { public int piece; public byte[] data; @@ -1796,6 +1684,16 @@ public void CancelRequestedPieces() thread.peer.CancelPieces(); } } + internal void StopWithError(string Message, string StackTrace = "") + { + status = Status.STOPPED; + + Log($"[CRITICAL ERROR] Msg: {Message}\r\n{StackTrace}"); StatusChanged?.Invoke(this, new StatusChangedArgs(2, Message + "\r\n"+ StackTrace)); + + Dispose(); + + throw new Exception(Message); + } internal void Log(string msg) { if (Options.Verbosity > 0) log.Write($"[BitSwarm] {msg}"); } #endregion } diff --git a/BitSwarm/BitSwarm.csproj b/BitSwarm/BitSwarm.csproj index f1cb60f..2fbba27 100644 --- a/BitSwarm/BitSwarm.csproj +++ b/BitSwarm/BitSwarm.csproj @@ -14,12 +14,19 @@ git bitswarm bittorrent torrent client streaming dht © SuRGeoNix 2020 - 2.2.8 - - Adding peer download rate stats -- Changing default download path from %temp% to %current% -- Organizing logs & clean-up of part files - 2.2.8.0 - 2.2.8.0 + 2.3.0 + - Implementing Save & Load/Restore Session (with Autonomous Advanced Part Files - APF) +- Implementing Export & Load Configuration (for BitSwarm's Options) +- Implementing Single Open for all BitSwarm's supported inputs (Torrent, Magnet, SHA1 Hash, Base32 Hash & Session) +- Organizing Folder/Files - Adding more folder paths options (Completed, Incompleted .apf, .Torrent, Session .bsf) +- Adding support for Env variables to be used with paths (format %var% even for unix platforms) +- Namespaces, Assemblies & Code Clean-up + 2.3.0.0 + 2.3.0.0 + SuRGeoNix.BitSwarmLib + BitSwarm + BitSwarmLib + BitSwarm @@ -31,6 +38,7 @@ + diff --git a/BitSwarm/Logger.cs b/BitSwarm/Logger.cs index be84038..9dfe28d 100644 --- a/BitSwarm/Logger.cs +++ b/BitSwarm/Logger.cs @@ -39,14 +39,14 @@ public void Write(string txt) public void Write1(string txt) { byte[] data = Encoding.UTF8.GetBytes($"[{sw.Elapsed.ToString(@"hh\:mm\:ss\:fff")}] {txt}\r\n"); - lock (locker) { if (disposed) return; fileStream.Write(data, 0, data.Length); } + lock (locker) { if (disposed) return; fileStream.Write(data, 0, data.Length); fileStream.Flush(); } } public void Write2(string txt) { if (disposed) return; byte[] data = Encoding.UTF8.GetBytes($"[{DateTime.Now.ToString("H.mm.ss.ffff")}] {txt}\r\n"); - lock (locker) { if (disposed) return; fileStream.Write(data, 0, data.Length); } + lock (locker) { if (disposed) return; fileStream.Write(data, 0, data.Length); fileStream.Flush(); } } public void Dispose() diff --git a/BitSwarm/Options.cs b/BitSwarm/Options.cs new file mode 100644 index 0000000..8aaf39d --- /dev/null +++ b/BitSwarm/Options.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Xml.Serialization; + +namespace SuRGeoNix.BitSwarmLib +{ + public class Options + { + public string FolderComplete { get; set; } = Directory.GetCurrentDirectory(); + public string FolderIncomplete { get; set; } = Path.Combine(Path.GetTempPath(), "BitSwarm", ".data"); + public string FolderTorrents { get; set; } = Path.Combine(Path.GetTempPath(), "BitSwarm", ".torrents"); + public string FolderSessions { get; set; } = Path.Combine(Path.GetTempPath(), "BitSwarm", ".sessions"); + + public int MaxThreads { get; set; } = 150; // Max Total Connection Threads | Short-Run + Long-Run + public int MinThreads { get; set; } = 15; // Max New Connection Threads | Short-Run + + public int BoostThreads { get; set; } = 60; // Max New Connection Threads | Boot Boost + public int BoostTime { get; set; } = 30; // Boot Boost Time (Seconds) + + + public int SleepModeLimit { get; set; } = 0; // Activates Sleep Mode (Low Resources) at the specify DownRate | DHT Stop, Re-Fills Stop (DHT/Trackers) & MinThreads Drop to MinThreads / 2 + // -1: Auto | 0: Disabled | Auto will figure out SleepModeLimit from MaxRate + + //public int DownloadLimit { get; set; } = -1; + //public int UploadLimit { get; set; } + + public int ConnectionTimeout { get; set; } = 600; + public int HandshakeTimeout { get; set; } = 800; + public int MetadataTimeout { get; set; } = 1600; + public int PieceTimeout { get; set; } = 1500; // Large timeouts without resets will cause more working pieces (more memory/more lost bytes on force stop) + public int PieceRetries { get; set; } = 3; + + public bool EnablePEX { get; set; } = true; + public bool EnableDHT { get; set; } = true; + public bool EnableTrackers { get; set; } = true; + public int PeersFromTracker { get; set; } = -1; + + public int BlockRequests { get; set; } = 9; // Blocks that we request at once for each peer (should be small on streaming to avoid delayed resets on timeouts) + + public int Verbosity { get; set; } = 0; // [0 - 4] + public bool LogTracker { get; set; } = false; // Verbosity 1 + public bool LogPeer { get; set; } = false; // Verbosity 1 - 4 + public bool LogDHT { get; set; } = false; // Verbosity 1 + public bool LogStats { get; set; } = false; // Verbosity 1 + + public string TrackersPath { get; set; } = ""; + + + public static string ConfigFile { get; private set; } = "bitswarm.config.xml"; + + public static void CreateConfig(Options opt, string path = null) + { + if (path == null) path = Directory.GetCurrentDirectory(); + + using (FileStream fs = new FileStream(Path.Combine(path, ConfigFile), FileMode.Create)) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(Options)); + xmlSerializer.Serialize(fs, opt); + } + } + public static Options LoadConfig(string customFilePath = null) + { + string foundPath; + + if (customFilePath != null) + { + if (!File.Exists(customFilePath)) return null; + foundPath = customFilePath; + } + else + { + foundPath = SearchConfig(); + if (foundPath == null) return null; + } + + using (FileStream fs = new FileStream(foundPath, FileMode.Open)) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(Options)); + return (Options) xmlSerializer.Deserialize(fs); + } + } + private static string SearchConfig() + { + string foundPath; + + foundPath = Path.Combine(Directory.GetCurrentDirectory(), ConfigFile); + if (File.Exists(foundPath)) return foundPath; + + if (Utils.IsWindows) + { + foundPath = Path.Combine(Environment.ExpandEnvironmentVariables("%APPDATA%") , "BitSwarm", ConfigFile); + if (File.Exists(foundPath)) return foundPath; + } + else + { + foundPath = Path.Combine(Environment.ExpandEnvironmentVariables("%HOME%") , ".bitswarm", ConfigFile); + if (File.Exists(foundPath)) return foundPath; + + foundPath = $"/etc/bitswarm/{ConfigFile}"; + if (File.Exists(foundPath)) return foundPath; + } + + return null; + } + } +} diff --git a/BitSwarm/PartFile.cs b/BitSwarm/PartFile.cs deleted file mode 100644 index 2ca763b..0000000 --- a/BitSwarm/PartFile.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace SuRGeoNix -{ - public class PartFile : IDisposable - { - public string FileName { get; private set; } - public long Size { get; private set; } - public int ChunkSize { get; private set; } - public int chunksCounter { get; private set; } - public bool FileCreated { get; private set; } - public bool AutoCreate { get { return autoCreate; } set { autoCreate = value; if (value && fileStream.Length == Size) CreateFile(); } } - private bool autoCreate; - - private FileStream fileStream; - private Dictionary mapIdToChunkId; - - private int firstPos; - private int firstChunkSize; - private int lastPos; - private int lastChunkSize; - - readonly object locker = new object(); - readonly object fileCreating = new object(); - - public PartFile(string fileName, int chunkSize, long size = -1, bool autoCreate = true) - { - if (File.Exists(fileName) || File.Exists(fileName + ".part")) throw new IOException("File " + fileName + " already exists"); - if (chunkSize < 1) throw new Exception("Chunk size must be > 0"); - - Directory.CreateDirectory(Path.GetDirectoryName(fileName)); - fileStream = File.Open(fileName + ".part", FileMode.CreateNew); - - FileName = fileName; - Size = size; - ChunkSize = chunkSize; - AutoCreate = autoCreate; - - mapIdToChunkId = new Dictionary(); - chunksCounter = -1; - firstChunkSize = 0; - lastChunkSize = 0; - firstPos = -1; - lastPos = -1; - } - public PartFile(string fileName, Dictionary mapIdToChunkId, int chunkSize, int firstPos, int firstChunkSize, int lastPos, int lastChunkSize, int chunksCounter) - { - // Currently not used | TODO for loading a session - - if (!File.Exists(fileName + ".part")) throw new IOException("File " + fileName + " not exists"); - if (chunkSize < 1) throw new Exception("Chunk size must be > 0"); - - fileStream = File.Open(fileName + ".part", FileMode.Open, FileAccess.Read); - - this.mapIdToChunkId = mapIdToChunkId; - this.FileName = fileName; - this.ChunkSize = chunkSize; - this.chunksCounter = chunksCounter; - this.firstChunkSize = firstChunkSize; - this.lastChunkSize = lastChunkSize; - this.firstPos = firstPos; - this.lastPos = lastPos; - } - - public void Write(int chunkId, byte[] chunk, int offset = 0) - { - lock (locker) - { - if (FileCreated) return; - if (mapIdToChunkId.ContainsKey(chunkId)) { Console.WriteLine($"ChunkId {chunkId} already written!"); return; } - - fileStream.Write(chunk, offset, (int) ChunkSize); - fileStream.Flush(); - chunksCounter++; - mapIdToChunkId.Add(chunkId, chunksCounter); - - if (AutoCreate && fileStream.Length == Size) CreateFile(); - } - } - public void WriteFirst(byte[] chunk, int offset, int len) - { - lock (locker) - { - if (FileCreated) return; - if (mapIdToChunkId.ContainsKey(0)) { Console.WriteLine($"ChunkId 0 already written!"); return; } - if (firstChunkSize != 0) throw new Exception("First chunk already exists"); - - fileStream.Write(chunk, offset, (int) len); - fileStream.Flush(); - chunksCounter++; - mapIdToChunkId.Add(0, chunksCounter); - firstChunkSize = len; - firstPos = chunksCounter; - - if (AutoCreate && fileStream.Length == Size) CreateFile(); - } - } - public void WriteLast(int chunkId, byte[] chunk, int offset, int len) - { - lock (locker) - { - if (FileCreated) return; - if (chunksCounter == -1 && chunkId == 0) { WriteFirst(chunk, offset, len); return; } // WriteLast as WriteFirst - if (mapIdToChunkId.ContainsKey(chunkId)) { Console.WriteLine($"ChunkId {chunkId} already written!"); return; } - - if (lastChunkSize != 0) throw new Exception("Last chunk already exists"); - - fileStream.Write(chunk, offset, (int) len); - fileStream.Flush(); - chunksCounter++; - mapIdToChunkId.Add(chunkId, chunksCounter); - lastChunkSize = len; - lastPos = chunksCounter; - - if (AutoCreate && fileStream.Length == Size) CreateFile(); - } - } - - public byte[] Read(long pos, long size) - { - lock (fileCreating) - { - if (firstPos == -1) return null; // Possible allow it for firstChunkSize == chunkSize | We dont even need firstPos to be able read - - byte[] retData = null; - - if (FileCreated) - { - retData = new byte[size]; - fileStream.Seek(pos, SeekOrigin.Begin); - fileStream.Read(retData, 0, (int)size); - return retData; - } - - long writeSize; - long startByte; - byte[] curChunk; - long sizeLeft = size; - - int chunkId = (int)((pos - firstChunkSize) / ChunkSize) + 1; - int lastChunkId = (int)((Size - firstChunkSize - 1) / ChunkSize) + 1; - - if (pos < firstChunkSize) - { - chunkId = 0; - startByte = pos; - writeSize = Math.Min(sizeLeft, firstChunkSize - startByte); - } - else if (chunkId == lastChunkId && lastPos != -1) - { - startByte = ((pos - firstChunkSize) % ChunkSize); - writeSize = Math.Min(sizeLeft, lastChunkSize - startByte); - } - else - { - startByte = ((pos - firstChunkSize) % ChunkSize); - writeSize = Math.Min(sizeLeft, ChunkSize - startByte); - } - - curChunk = ReadChunk(chunkId); - retData = Utils.ArraySub(ref curChunk, (uint)startByte, (uint)writeSize); - sizeLeft -= writeSize; - - while (sizeLeft > 0) - { - chunkId++; - - curChunk = ReadChunk(chunkId); - if (chunkId == lastChunkId && lastPos != -1) - writeSize = (uint)Math.Min(sizeLeft, lastChunkSize); - else - writeSize = (uint)Math.Min(sizeLeft, ChunkSize); - retData = Utils.ArrayMerge(retData, Utils.ArraySub(ref curChunk, 0, (uint)writeSize)); - sizeLeft -= writeSize; - } - - return retData; - } - } - public byte[] ReadChunk(int chunkId) - { - long pos = 0; - int len = ChunkSize; - - lock (locker) - { - if (FileCreated) return null; - - if (chunkId == 0 && firstChunkSize != 0) - len = firstChunkSize; - else if (chunkId == (int)((Size - firstChunkSize - 1) / ChunkSize) + 1 && lastChunkSize != 0) - len = lastChunkSize; - - int chunkPos = mapIdToChunkId[chunkId]; - int chunkPos2 = chunkPos; - if (firstChunkSize != 0 && chunkPos > firstPos) { pos += firstChunkSize; chunkPos2--; } - if (lastChunkSize != 0 && chunkPos > lastPos ) { pos += lastChunkSize; chunkPos2--; } - pos += (long)ChunkSize * chunkPos2; - - if (pos < fileStream.Length) - { - byte[] data = new byte[len]; - long savePos = fileStream.Position; - try - { - fileStream.Seek(pos, SeekOrigin.Begin); - fileStream.Read(data, 0, len); - fileStream.Seek(savePos, SeekOrigin.Begin); - return data; - } - catch (Exception e) { fileStream.Seek(savePos, SeekOrigin.Begin); throw e; } - } - } - - // pos >= fileStream.Length - throw new Exception("Data not available"); - } - - public void CreateFile() - { - lock (fileCreating) - { - lock (locker) - { - if (FileCreated) return; - - using (FileStream fs = File.Open(FileName, FileMode.CreateNew)) - { - if (Size > 0) - { - for (int i = 0; i <= chunksCounter; i++) - { - byte[] data = ReadChunk(i); - fs.Write(data, 0, data.Length); - } - } - - FileCreated = true; - } - fileStream.Close(); - fileStream = File.Open(FileName, FileMode.Open, FileAccess.Read); - File.Delete(FileName + ".part"); - } - } - } - public void Dispose() - { - if (fileStream != null) - { - //bool deleteFile = fileStream.Length == 0; - bool deleteFile = true; - fileStream.Flush(); - fileStream.Close(); - if (deleteFile) File.Delete(FileName + ".part"); - } - } - } -} \ No newline at end of file diff --git a/BitSwarm/Stats.cs b/BitSwarm/Stats.cs new file mode 100644 index 0000000..029fe72 --- /dev/null +++ b/BitSwarm/Stats.cs @@ -0,0 +1,57 @@ +namespace SuRGeoNix.BitSwarmLib +{ + public class Stats + { + public int DownRate { get; internal set; } + public int AvgRate { get; internal set; } + public int MaxRate { get; internal set; } + + public int AvgETA { get; internal set; } + public int ETA { get; internal set; } + + public int Progress { get; internal set; } + public int PiecesIncluded { get; internal set; } + public long BytesIncluded { get; internal set; } + public long BytesCurDownloaded { get; internal set; } // Bytes Saved in Files + + public long BytesDownloaded { get; internal set; } // Bytes Saved in Files + Working Pieces + public long BytesDownloadedPrev { get; internal set; } + public long BytesDownloadedPrevSession { get; internal set; } // Bytes Saved in Files Previously + public long BytesUploaded { get; internal set; } + public long BytesDropped { get; internal set; } + + public int PeersTotal { get; internal set; } + public int PeersInQueue { get; internal set; } + public int PeersConnecting { get; internal set; } + public int PeersConnected { get; internal set; } + public int PeersFailed1 { get; internal set; } + public int PeersFailed2 { get; internal set; } + public int PeersFailed { get; internal set; } + public int PeersChoked { get; internal set; } + public int PeersUnChoked { get; internal set; } + public int PeersDownloading { get; internal set; } + public int PeersDropped { get; internal set; } + + public long StartTime { get; internal set; } + public long CurrentTime { get; internal set; } + public long EndTime { get; internal set; } + + public bool SleepMode { get; internal set; } + public bool BoostMode { get; internal set; } + public bool EndGameMode { get; internal set; } + + public int AlreadyReceived { get; internal set; } + + public int DHTPeers { get; internal set; } + public int PEXPeers { get; internal set; } + public int TRKPeers { get; internal set; } + + public int SHA1Failures { get; internal set; } + + public int Rejects; + + //public int ConnectTimeouts; // Not used + public int HandshakeTimeouts; + public int PieceTimeouts; + } +} diff --git a/BitSwarm/ThreadPool.cs b/BitSwarm/ThreadPool.cs new file mode 100644 index 0000000..2a131cd --- /dev/null +++ b/BitSwarm/ThreadPool.cs @@ -0,0 +1,138 @@ +using System.Collections.Concurrent; +using System.Threading; + +using SuRGeoNix.BitSwarmLib.BEP; + +namespace SuRGeoNix.BitSwarmLib +{ + /// + /// BitSwarm's Thread Pool For Peers Dispatching [Short/Long Run] + /// + class ThreadPool + { + public bool Stop { get; internal set; } + public int MaxThreads { get; private set; } + public int MinThreads { get; private set; } + public int Available => MaxThreads- Running; + public int Running; + public int ShortRun => Running - LongRun; + public int LongRun; + + public ThreadPeer[] Threads; + + ConcurrentStack peersForDispatch; + private readonly object lockerThreads = new object(); + + public void Initialize(int minThreads, int maxThreads, ConcurrentStack peersStack) + { + lock (lockerThreads) + { + Dispose(); + + Stop = false; + MinThreads = minThreads; + MaxThreads = maxThreads; + Running = maxThreads; + Threads = new ThreadPeer[MaxThreads]; + + peersForDispatch = peersStack; + + for (int i=0; i { ThreadRun(cacheI); }); + Threads[i].thread.IsBackground = true; + Threads[i].thread.Start(); + } + private void ThreadRun(int index) + { + Interlocked.Decrement(ref Running); + Threads[index].IsAlive = true; + + while (!Stop) + { + Threads[index].resetEvent.WaitOne(); + if (Stop) break; + + do + { + Threads[index].peer?.Run(this, index); + if (ShortRun > MinThreads || Stop || Threads == null || Threads[index] == null) break; + lock (peersForDispatch) + if (peersForDispatch.TryPop(out Peer tmp)) { Threads[index].peer = tmp; Threads[index].peer.status = Peer.Status.CONNECTING; } else break; + + } while (true); + + if (Threads != null && Threads[index] != null) Threads[index].IsRunning = false; + Interlocked.Decrement(ref Running); + } + } + + public bool Dispatch(Peer peer) + { + lock (lockerThreads) + { + if (Stop || Running >= MaxThreads || ShortRun >= MinThreads) return false; + + foreach (var thread in Threads) + if (thread != null && !thread.IsRunning && thread.IsAlive) + { + if (Running >= MaxThreads || ShortRun >= MinThreads) return false; + + if (peer != null) peer.status = Peer.Status.CONNECTING; + thread.peer = peer; + thread.IsRunning= true; + Interlocked.Increment(ref Running); + thread.resetEvent.Set(); + + return true; + } + + return false; + } + } + public void Dispose() + { + lock (lockerThreads) + { + if (peersForDispatch != null) lock (peersForDispatch) peersForDispatch.Clear(); + Stop = true; + + if (Threads != null) + { + foreach (var thread in Threads) thread?.resetEvent.Set(); + + int escape = 150; + while (Running > 0 && escape > 0) { Thread.Sleep(20); escape--; } + } + + MinThreads = 0; + MaxThreads = 0; + Running = 0; + Threads = null; + } + } + } + + class ThreadPeer + { + public AutoResetEvent resetEvent = new AutoResetEvent(false); + public bool isLongRun { get; internal set; } + public bool IsRunning { get; internal set; } + public bool IsAlive { get; internal set; } + + public Thread thread; + public Peer peer; + } +} diff --git a/BitSwarm/Torrent.cs b/BitSwarm/Torrent.cs deleted file mode 100644 index 7ed3d4e..0000000 --- a/BitSwarm/Torrent.cs +++ /dev/null @@ -1,331 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Security.Cryptography; -using System.Text.RegularExpressions; -using System.Web; - -using BencodeNET.Parsing; -using BencodeNET.Objects; - -namespace SuRGeoNix.BEP -{ - public class Torrent : IDisposable - { - public TorrentFile file; - public TorrentData data; - public MetaData metadata; - - public string DownloadPath { get; set; } - public bool isMultiFile { get; private set; } - - public struct TorrentFile - { - // SHA-1 of 'info' - public string infoHash { get; set; } - - // 'announce' | 'announce-list' - public List trackers { get; set; } - - // 'info' - - // 'name' | 'length' - public string name { get; set; } - public long length { get; set; } - - // ['path' | 'length'] - public List paths { get; set; } - public List lengths { get; set; } - - public int pieceLength { get; set; } - public List pieces { get; set; } - } - public struct TorrentData - { - public bool isDone { get; set; } - - public List files { get; set; } - public List filesIncludes { get; set; } - public string folder { get; set; } - public long totalSize { get; set; } - - public int pieces { get; set; } - public int pieceSize { get; set; } - public int pieceLastSize { get; set; } // NOTE: it can be 0, it should be equals with pieceSize in case of totalSize % pieceSize = 0 - - public int blocks { get; set; } - public int blockSize { get; set; } - public int blockLastSize { get; set; } - public int blocksLastPiece { get; set; } - - public BitField progress { get; set; } - public BitField requests { get; set; } - public BitField progressPrev { get; set; } - - - internal Dictionary pieceProgress; - internal class PieceProgress - { - public PieceProgress(ref TorrentData data, int piece) - { - bool isLastPiece= piece == data.pieces - 1 && data.totalSize % data.pieceSize != 0; - - this.piece = piece; - this.data = !isLastPiece ? new byte[data.pieceSize] : new byte[data.pieceLastSize]; - this.progress = !isLastPiece ? new BitField(data.blocks): new BitField(data.blocksLastPiece); - this.requests = !isLastPiece ? new BitField(data.blocks): new BitField(data.blocksLastPiece); - } - public int piece; - public byte[] data; - public BitField progress; - public BitField requests; - } - } - public struct MetaData - { - public bool isDone { get; set; } - - public PartFile file { get; set; } - public int pieces { get; set; } - public long totalSize { get; set; } - - public BitField progress { get; set; } - - public int parallelRequests { get; set; } - } - - private static BencodeParser bParser = new BencodeParser(); - public static SHA1 sha1 = new SHA1Managed(); - - public Torrent (string downloadPath) - { - DownloadPath = downloadPath; - - file = new TorrentFile(); - data = new TorrentData(); - metadata = new MetaData(); - - file.trackers = new List(); - } - - public void FillFromMagnetLink(Uri magnetLink) - { - // TODO: Check v2 Magnet Link - // http://www.bittorrent.org/beps/bep_0009.html - - NameValueCollection nvc = HttpUtility.ParseQueryString(magnetLink.Query); - string[] xt = nvc.Get("xt") == null ? null : nvc.GetValues("xt")[0].Split(Char.Parse(":")); - if (xt == null || xt.Length != 3 || xt[1].ToLower() != "btih" || xt[2].Length < 20) throw new Exception("[Magnet][xt] No hash found " + magnetLink); - - file.name = nvc.Get("dn") == null ? null : nvc.GetValues("dn")[0] ; - file.length = nvc.Get("xl") == null ? 0 : (int) UInt32.Parse(nvc.GetValues("xl")[0]); - file.infoHash = xt[2]; - - // Base32 Hash - if (file.infoHash.Length != 40) - { - if (Regex.IsMatch(file.infoHash,@"[QAZ2WSX3EDC4RFV5TGB6YHN7UJM8K9LP]+")) - { - try - { - file.infoHash = Utils.ArrayToStringHex(Utils.FromBase32String(file.infoHash)); - if (file.infoHash.Length != 40) throw new Exception("[Magnet][xt] No valid hash found " + magnetLink); - } catch (Exception) { throw new Exception("[Magnet][xt] No valid hash found " + magnetLink); } - } else { throw new Exception("[Magnet][xt] No valid hash found " + magnetLink); } - } - - string[] tr = nvc.Get("tr") == null ? null : nvc.GetValues("tr"); - if (tr == null) return; - - for (int i=0; i(fileName); - BDictionary bInfo; - - if (bdicTorrent["info"] != null) - { - bInfo = (BDictionary) bdicTorrent["info"]; - FillTrackersFromInfo(bdicTorrent); - } - else if (bdicTorrent["name"] != null) - bInfo = bdicTorrent; - else - throw new Exception("Invalid torrent file"); - - file.infoHash = Utils.ArrayToStringHex(sha1.ComputeHash(bInfo.EncodeAsBytes())); - FillFromInfo(bInfo); - } - public void FillFromMetadata() - { - if (metadata.file == null) throw new Exception("No metadata found"); - - BDictionary bInfo = (BDictionary) bParser.Parse(metadata.file.FileName); - FillFromInfo(bInfo); - - if (file.infoHash != Utils.ArrayToStringHex(sha1.ComputeHash(bInfo.EncodeAsBytes()))) - Console.WriteLine("CRITICAL!!!! Metadata SHA1 validation failed"); - } - public void FillFromInfo(BDictionary bInfo) - { - if (DownloadPath == null) throw new Exception("DownloadPath cannot be empty"); - - isMultiFile = (BList) bInfo["files"] == null ? false : true; - - file.name = ((BString) bInfo["name"]).ToString(); - file.pieces = GetHashesFromInfo(bInfo); - file.pieceLength = (BNumber) bInfo["piece length"]; - - data.files = new List(); - data.filesIncludes = new List(); - - if (isMultiFile) - { - file.paths = GetPathsFromInfo(bInfo); - file.lengths = GetFileLengthsFromInfo(bInfo, out long tmpTotalSize); - data.totalSize = tmpTotalSize; - - data.folder = Utils.FindNextAvailableDir(Path.Combine(DownloadPath, file.name)).Replace("..","_"); - for (int i=0; i() { file.name }; - file.lengths = new List() { file.length }; - - data.filesIncludes.Add(file.name); - } - - data.pieces = file.pieces.Count; - data.pieceSize = file.pieceLength; - data.pieceLastSize = (int) (data.totalSize % data.pieceSize); // NOTE: it can be 0, it should be equals with pieceSize in case of totalSize % pieceSize = 0 - - data.progress = new BitField(data.pieces); - data.requests = new BitField(data.pieces); - data.progressPrev = new BitField(data.pieces); - - data.blockSize = Math.Min(Peer.MAX_DATA_SIZE, data.pieceSize); - data.blocks = ((data.pieceSize -1) / data.blockSize) + 1; - data.blockLastSize = data.pieceLastSize % data.blockSize == 0 ? data.blockSize : data.pieceLastSize % data.blockSize; - data.blocksLastPiece= ((data.pieceLastSize -1) / data.blockSize) + 1; - - data.pieceProgress = new Dictionary(); - } - - public void FillTrackersFromTrackersPath(string fileName) - { - try - { - if (fileName == null || fileName.Trim() == "" || !File.Exists(fileName)) return; - - string[] trackers = File.ReadAllLines(fileName); - - foreach (var tracker in trackers) - try { file.trackers.Add(new Uri(tracker)); } catch (Exception) { } - } catch (Exception) { } - } - public void FillTrackersFromInfo(BDictionary torrent) - { - string tracker = null; - BList trackersBList = null; - - if (torrent["announce"] != null) - tracker = ((BString) torrent["announce"]).ToString(); - - if (torrent["announce-list"] != null) - trackersBList = (BList) torrent["announce-list"]; - - if (trackersBList != null) - for (int i=0; i GetPathsFromInfo(BDictionary info) - { - BList files = (BList) info["files"]; - if (files == null) return null; - - List fileNames = new List(); - - for (int i=0; i GetFileLengthsFromInfo(BDictionary info, out long totalSize) - { - totalSize = 0; - - BList files = (BList) info["files"]; - if (files == null) return null; - List lens = new List(); - - for (int i=0; i GetHashesFromInfo(BDictionary info) - { - byte[] hashBytes = ((BString) info["pieces"]).Value.ToArray(); - List hashes = new List(); - - for (int i=0; i