diff --git a/GChan/Controllers/MainController.cs b/GChan/Controllers/MainController.cs index 6e06f67..f8e967a 100644 --- a/GChan/Controllers/MainController.cs +++ b/GChan/Controllers/MainController.cs @@ -1,5 +1,6 @@ using GChan.Data; using GChan.Forms; +using GChan.Helpers; using GChan.Models; using GChan.Properties; using GChan.Trackers; @@ -82,7 +83,7 @@ public MainController(MainForm mainForm) scanTimer.Enabled = false; scanTimer.Interval = Settings.Default.ScanTimer; - scanTimer.Tick += new EventHandler(Scan); + scanTimer.Tick += new EventHandler(StartScanThread); } public void LoadTrackers() @@ -106,7 +107,6 @@ public void LoadTrackers() new SysThread(() => { - // END TODO Parallel.ForEach(threads, (thread) => { Thread newThread = (Thread)Utils.CreateNewTracker(thread); @@ -114,16 +114,16 @@ public void LoadTrackers() }); Form.Invoke((MethodInvoker)delegate { - Done(); + FinishLoadingTrackers(); }); }).Start(); } /// Executed once everything has finished being loaded. - void Done() + void FinishLoadingTrackers() { scanTimer.Enabled = true; - Scan(this, new EventArgs()); + StartScanThread(this, new EventArgs()); // Check for updates. if (Settings.Default.CheckForUpdatesOnStart) @@ -133,41 +133,56 @@ void Done() } } - public void AddUrl(string url) + public void AddUrls(IEnumerable urls) { - Tracker newTracker = Utils.CreateNewTracker(url); - - if (newTracker != null) - { - List trackerList = ((newTracker.Type == Type.Board) ? Model.Boards.Cast() : Model.Threads.Cast()).ToList(); + bool trackerWasAdded = false; + foreach (var url in urls) + { + var newTracker = Utils.CreateNewTracker(url); - if (IsUnique(newTracker, trackerList)) - { - AddNewTracker(newTracker); - scanTimer.Enabled = true; - Scan(this, new EventArgs()); - } - else + if (newTracker != null) { - DialogResult result = MessageBox.Show( - "URL is already being tracked!\nOpen corresponding folder?", - "Error", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1); + var trackerList = ((newTracker.Type == Type.Board) ? Model.Boards.Cast() : Model.Threads.Cast()).ToArray(); - if (result == DialogResult.Yes) + if (IsUnique(newTracker, trackerList)) + { + var addSuccess = AddNewTracker(newTracker); + if (addSuccess) + { + trackerWasAdded = true; + } + } + else { - string spath = newTracker.SaveTo; - if (!Directory.Exists(spath)) - Directory.CreateDirectory(spath); - Process.Start(spath); + var result = MessageBox.Show( + "URL is already being tracked!\nOpen corresponding folder?", + "Error", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1); + + if (result == DialogResult.Yes) + { + var path = newTracker.SaveTo; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + Process.Start(path); + } } } + else + { + MessageBox.Show($"Entered text '{url}' is not a supported site or board/thread!"); + } } - else + + if (trackerWasAdded) { - MessageBox.Show($"Entered text '{url}' is not a supported site or board/thread!"); + scanTimer.Enabled = true; + StartScanThread(this, new EventArgs()); } } + /// Was added to a tracker list. private bool AddNewTracker(Tracker tracker) { if (tracker == null) @@ -235,7 +250,10 @@ internal void ClearTrackers(Type type) } } - private void Scan(object sender, EventArgs e) + /// + /// Run the scan thread if it isn't already running. + /// + private void StartScanThread(object sender, EventArgs e) { if (scanThread == null || !scanThread.IsAlive) { @@ -249,67 +267,71 @@ private void Scan(object sender, EventArgs e) } } + /// + /// Thread entry-point. + /// private void ScanRoutine() { lock (ThreadLock) { // Remove 404'd threads - for (int i = 0; i < Model.Threads.Count; i++) - { - if (Model.Threads[i].Gone) - { - RemoveThread(Model.Threads[i]); - i--; - } - } + var removedThreads = Model.Threads.RemoveAll(t => t.Gone); + removedThreads.ForEach(t => RemoveThread(t)); } // Make a copy of the current boards and scrape them for new threads. - var boards = Model.Boards.ToList(); + var boards = Model.Boards.ToArray(); - for (int i = 0; i < boards.Count; i++) + foreach (var board in boards) { - if (boards[i].Scraping) + if (board.Scraping) { - // OrderBy because we need the threads to be in ascending order by ID for LargestAddedThreadNo to be useful. - string[] boardThreadUrls = boards[i].GetThreadLinks().OrderBy(t => t).ToArray(); - int largestNo = 0; + var threadUrls = board.GetThreadLinks(); + var greatestThreadIdLock = new object(); + var greatestThreadId = 0; - Parallel.ForEach(boardThreadUrls, (url) => + Parallel.ForEach(threadUrls, (threadUrl) => { - if (boards[i].Scraping) + if (board.Scraping) { - int? id = GetThreadId(boards[i], url); + var id = GetThreadId(board, threadUrl); - if (id.HasValue && id.Value > boards[i].LargestAddedThreadNo) + if (id != null && id > board.LargestAddedThreadNo) { - Thread newThread = (Thread)Utils.CreateNewTracker(url); + var newThread = (Thread)Utils.CreateNewTracker(threadUrl); if (newThread != null && IsUnique(newThread, Model.Threads)) { - bool urlWasAdded = AddNewTracker(newThread); + var urlWasAdded = AddNewTracker(newThread); if (urlWasAdded) { - if (id.Value > largestNo) //Not exactly safe in multithreaded but should work fine. - largestNo = id.Value; + lock (greatestThreadIdLock) + { + if (id > greatestThreadId) + { + greatestThreadId = id.Value; + } + } } } } } }); - boards[i].LargestAddedThreadNo = largestNo; + board.LargestAddedThreadNo = greatestThreadId; } } // Make a copy of the current threads and download them. - var threads = Model.Threads.ToList(); + var threads = Model.Threads.ToArray(); - for (int i = 0; i < threads.Count; i++) + for (int i = 0; i < threads.Length; i++) { if (threads[i].Scraping) + { ThreadPool.QueueUserWorkItem(new WaitCallback(threads[i].Download)); + } } } @@ -319,8 +341,8 @@ private void ScanRoutine() { var idCodeMatch = board.SiteName switch { - Thread_4Chan.ID_CODE_REGEX => Regex.Match(url, Thread_4Chan.ID_CODE_REGEX), - Board_8Kun.SITE_NAME_8KUN => Regex.Match(url, Thread_8Kun.ID_CODE_REGEX), + Board_4Chan.SITE_NAME => Regex.Match(url, Thread_4Chan.ID_CODE_REGEX), + Board_8Kun.SITE_NAME => Regex.Match(url, Thread_8Kun.ID_CODE_REGEX), _ => null, }; @@ -342,12 +364,13 @@ private void ScanRoutine() /// private bool IsUnique(Tracker tracker, IEnumerable list) { - if (tracker.Type == Type.Thread) + if (tracker is Thread thread) { - return !list.OfType().Any( - t => t.SiteName == tracker.SiteName && - t.BoardCode == tracker.BoardCode && - t.ID == ((Thread)tracker).ID); + return !list.OfType().Any(t => + t.SiteName == thread.SiteName && + t.BoardCode == thread.BoardCode && + t.ID == thread.ID + ); } else // Board { @@ -475,8 +498,8 @@ public bool Closing() scanTimer.Dispose(); if (Settings.Default.SaveListsOnClose) - { - DataController.SaveAll(Model.Threads.ToList(), Model.Boards); + { + DataController.SaveAll(Model.Threads.ToArray(), Model.Boards.ToArray()); } return false; diff --git a/GChan/Controls/SortableBindingList/SortableBindingList.cs b/GChan/Controls/SortableBindingList/SortableBindingList.cs index aa9dcce..eb6924a 100644 --- a/GChan/Controls/SortableBindingList/SortableBindingList.cs +++ b/GChan/Controls/SortableBindingList/SortableBindingList.cs @@ -1,9 +1,15 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Drawing.Imaging; +using System.Linq; namespace GChan.Controls { + /// + /// Binding list that gives list changed events, also supports sorting for the WinForms UI. + /// Attempts to be thread-safe as possible within reason. + /// public class SortableBindingList : BindingList { private readonly Dictionary> comparers; @@ -11,6 +17,8 @@ public class SortableBindingList : BindingList private ListSortDirection listSortDirection; private PropertyDescriptor propertyDescriptor; + private readonly object ilock = new(); + public SortableBindingList() : base(new List()) { @@ -48,6 +56,32 @@ protected override bool SupportsSearchingCore get { return true; } } + public new void Add(T item) + { + lock (ilock) + { + base.Add(item); + } + } + + public new T this[int index] + { + get + { + lock (ilock) + { + return base[index]; + } + } + set + { + lock (ilock) + { + base[index] = value; + } + } + } + protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) { List itemsList = (List)Items; @@ -93,5 +127,89 @@ protected override int FindCore(PropertyDescriptor property, object key) return -1; } + + /// Item removed from . + public new T RemoveAt(int index) + { + lock (ilock) + { + var item = this[index]; + base.RemoveAt(index); + return item; + } + } + + public new void Remove(T item) + { + lock (ilock) + { + if (Contains(item)) + { + base.Remove(item); + } + } + } + + /// Removed items. + public T[] RemoveAll(Predicate match) + { + lock (ilock) + { + var itemsToRemove = new List(); + + foreach (T item in this) + { + if (match(item)) + { + itemsToRemove.Add(item); + } + } + + foreach (T item in itemsToRemove) + { + this.Remove(item); + } + + return itemsToRemove.ToArray(); + } + } + + /// + /// Lock the list and foreach loop over the items (don't do long operations with this). + /// + public void ForEachLocked(Action action) + { + lock (ilock) + { + foreach (var item in this) + { + action(item); + } + } + } + + /// + /// Thread Safe implementation. + /// + public List ToList() + { + lock (ilock) + { + return Enumerable.ToList(this); + } + } + + /// + /// Thread Safe implementation. + /// + public T[] ToArray() + { + lock (ilock) + { + return Enumerable.ToArray(this); + } + } + + public T[] Copy() => ToArray(); } } \ No newline at end of file diff --git a/GChan/Forms/AboutBox.resx b/GChan/Forms/AboutBox.resx index cc27825..434a569 100644 --- a/GChan/Forms/AboutBox.resx +++ b/GChan/Forms/AboutBox.resx @@ -1,4 +1,4 @@ - +