Comment annuler une tâche en attente après un délai d’attente

J’utilise cette méthode pour instancier un navigateur Web par programme, naviguer vers une URL et renvoyer un résultat à la fin du document.

Comment pourrais-je arrêter la Task et demander à GetFinalUrl() renvoyer null si le document nécessite plus de 5 secondes pour se charger?

J’ai vu de nombreux exemples utilisant une TaskFactory mais je n’ai pas pu l’appliquer à ce code.

  private Uri GetFinalUrl(PortalMerchant portalMerchant) { SetBrowserFeatureControl(); Uri finalUri = null; if (ssortingng.IsNullOrEmpty(portalMerchant.Url)) { return null; } Uri trackingUrl = new Uri(portalMerchant.Url); var task = MessageLoopWorker.Run(DoWorkAsync, trackingUrl); task.Wait(); if (!Ssortingng.IsNullOrEmpty(task.Result.ToSsortingng())) { return new Uri(task.Result.ToSsortingng()); } else { throw new Exception("Parsing Failed"); } } // by Noseratio - http://stackoverflow.com/users/1768303/noseratio static async Task DoWorkAsync(object[] args) { _threadCount++; Console.WriteLine("Thread count:" + _threadCount); Uri retVal = null; var wb = new WebBrowser(); wb.ScriptErrorsSuppressed = true; TaskCompletionSource tcs = null; WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e) => tcs.TrySetResult(true); foreach (var url in args) { tcs = new TaskCompletionSource(); wb.DocumentCompleted += documentCompletedHandler; try { wb.Navigate(url.ToSsortingng()); await tcs.Task; } finally { wb.DocumentCompleted -= documentCompletedHandler; } retVal = wb.Url; wb.Dispose(); return retVal; } return null; } public static class MessageLoopWorker { #region Public static methods public static async Task Run(Func<object[], Task> worker, params object[] args) { var tcs = new TaskCompletionSource(); var thread = new Thread(() => { EventHandler idleHandler = null; idleHandler = async (s, e) => { // handle Application.Idle just once Application.Idle -= idleHandler; // return to the message loop await Task.Yield(); // and continue asynchronously // propogate the result or exception try { var result = await worker(args); tcs.SetResult(result); } catch (Exception ex) { tcs.SetException(ex); } // signal to exit the message loop // Application.Run will exit at this point Application.ExitThread(); }; // handle Application.Idle just once // to make sure we're inside the message loop // and SynchronizationContext has been correctly installed Application.Idle += idleHandler; Application.Run(); }); // set STA model for the new thread thread.SetApartmentState(ApartmentState.STA); // start the thread and await for the task thread.Start(); try { return await tcs.Task; } finally { thread.Join(); } } #endregion } 

Mise à jour : Github contient la dernière version de l’outil d’parsing Web de la console basée sur WebBrowser .

Mis à jour : Ajout d’un pool d’objects WebBrowser pour plusieurs téléchargements parallèles.

Avez-vous un exemple de comment faire cela dans une application console par hasard? De plus, je ne pense pas que webBrowser puisse être une variable de classe car je lance le tout dans un parallèle pour chaque itération, en itérant des milliers d’URL.

Vous trouverez ci-dessous une implémentation d’ un scrapper Web plus ou moins générique basé sur WebBrowser , qui fonctionne comme une application console. C’est une consolidation de certains de mes précédents efforts liés à WebBrowser , y compris le code référencé dans la question:

  • Capturer une image de la page Web avec une opacité

  • Chargement d’une page avec du contenu AJAX dynamic

  • Création d’un thread de boucle de message STA pour WebBrowser

  • Chargement d’un ensemble d’URL, l’un après l’autre

  • Impression d’un ensemble d’URL avec WebBrowser

  • Automatisation de l’interface utilisateur de la page Web

Quelques points:

  • La classe MessageLoopApartment réutilisable est utilisée pour démarrer et exécuter un thread WinForms STA avec sa propre pompe de messages. Il peut être utilisé à partir d’une application console , comme ci-dessous. Cette classe expose un planificateur de tâches TPL ( FromCurrentSynchronizationContext ) et un ensemble de wrappers Task.Factory.StartNew pour utiliser ce planificateur de tâches.

  • Cela rend async/await un excellent outil pour exécuter des tâches de navigation WebBrowser sur ce thread STA distinct. De cette façon, un object WebBrowser est créé, navigué et détruit sur ce fil. Bien que MessageLoopApartment ne soit pas spécifiquement lié à WebBrowser .

  • Il est important d’activer le rendu HTML5 à l’aide du Contrôle des fonctionnalités du navigateur , car sinon, les objects WebBrowser s’exécutent en mode d’émulation IE7 par défaut. C’est ce que SetFeatureBrowserEmulation fait ci-dessous.

  • Il n’est pas toujours possible de déterminer quand une page Web a fini de s’afficher avec une probabilité de 100%. Certaines pages sont assez complexes et utilisent des mises à jour AJAX continues. Cependant, nous pouvons être assez proches en traitant d’abord l’événement DocumentCompleted , puis en interrogeant l’instantané HTML actuel de la page pour connaître les modifications et en vérifiant la propriété WebBrowser.IsBusy . C’est ce que NavigateAsync fait ci-dessous.

  • Une logique de délai d’attente est présente en plus de ce qui précède, au cas où le rendu de la page serait sans fin (notez CancellationTokenSource et CreateLinkedTokenSource ).

 using Microsoft.Win32; using System; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace Console_22239357 { class Program { // by Noseratio - https://stackoverflow.com/a/22262976/1768303 // main logic static async Task ScrapSitesAsync(ssortingng[] urls, CancellationToken token) { using (var apartment = new MessageLoopApartment()) { // create WebBrowser inside MessageLoopApartment var webBrowser = apartment.Invoke(() => new WebBrowser()); try { foreach (var url in urls) { Console.WriteLine("URL:\n" + url); // cancel in 30s or when the main token is signalled var navigationCts = CancellationTokenSource.CreateLinkedTokenSource(token); navigationCts.CancelAfter((int)TimeSpan.FromSeconds(30).TotalMilliseconds); var navigationToken = navigationCts.Token; // run the navigation task inside MessageLoopApartment ssortingng html = await apartment.Run(() => webBrowser.NavigateAsync(url, navigationToken), navigationToken); Console.WriteLine("HTML:\n" + html); } } finally { // dispose of WebBrowser inside MessageLoopApartment apartment.Invoke(() => webBrowser.Dispose()); } } } // entry point static void Main(ssortingng[] args) { try { WebBrowserExt.SetFeatureBrowserEmulation(); // enable HTML5 var cts = new CancellationTokenSource((int)TimeSpan.FromMinutes(3).TotalMilliseconds); var task = ScrapSitesAsync( new[] { "http://example.com", "http://example.org", "http://example.net" }, cts.Token); task.Wait(); Console.WriteLine("Press Enter to exit..."); Console.ReadLine(); } catch (Exception ex) { while (ex is AggregateException && ex.InnerException != null) ex = ex.InnerException; Console.WriteLine(ex.Message); Environment.Exit(-1); } } } ///  /// WebBrowserExt - WebBrowser extensions /// by Noseratio - https://stackoverflow.com/a/22262976/1768303 ///  public static class WebBrowserExt { const int POLL_DELAY = 500; // navigate and download public static async Task NavigateAsync(this WebBrowser webBrowser, ssortingng url, CancellationToken token) { // navigate and await DocumentCompleted var tcs = new TaskCompletionSource(); WebBrowserDocumentCompletedEventHandler handler = (s, arg) => tcs.TrySetResult(true); using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true)) { webBrowser.DocumentCompleted += handler; try { webBrowser.Navigate(url); await tcs.Task; // wait for DocumentCompleted } finally { webBrowser.DocumentCompleted -= handler; } } // get the root element var documentElement = webBrowser.Document.GetElementsByTagName("html")[0]; // poll the current HTML for changes asynchronosly var html = documentElement.OuterHtml; while (true) { // wait asynchronously, this will throw if cancellation requested await Task.Delay(POLL_DELAY, token); // continue polling if the WebBrowser is still busy if (webBrowser.IsBusy) continue; var htmlNow = documentElement.OuterHtml; if (html == htmlNow) break; // no changes detected, end the poll loop html = htmlNow; } // consider the page fully rendered token.ThrowIfCancellationRequested(); return html; } // enable HTML5 (assuming we're running IE10+) // more info: https://stackoverflow.com/a/18333982/1768303 public static void SetFeatureBrowserEmulation() { if (System.ComponentModel.LicenseManager.UsageMode != System.ComponentModel.LicenseUsageMode.Runtime) return; var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName); Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION", appName, 10000, RegistryValueKind.DWord); } } ///  /// MessageLoopApartment /// STA thread with message pump for serial execution of tasks /// by Noseratio - https://stackoverflow.com/a/22262976/1768303 ///  public class MessageLoopApartment : IDisposable { Thread _thread; // the STA thread TaskScheduler _taskScheduler; // the STA thread's task scheduler public TaskScheduler TaskScheduler { get { return _taskScheduler; } } /// MessageLoopApartment constructor public MessageLoopApartment() { var tcs = new TaskCompletionSource(); // start an STA thread and gets a task scheduler _thread = new Thread(startArg => { EventHandler idleHandler = null; idleHandler = (s, e) => { // handle Application.Idle just once Application.Idle -= idleHandler; // return the task scheduler tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext()); }; // handle Application.Idle just once // to make sure we're inside the message loop // and SynchronizationContext has been correctly installed Application.Idle += idleHandler; Application.Run(); }); _thread.SetApartmentState(ApartmentState.STA); _thread.IsBackground = true; _thread.Start(); _taskScheduler = tcs.Task.Result; } /// shutdown the STA thread public void Dispose() { if (_taskScheduler != null) { var taskScheduler = _taskScheduler; _taskScheduler = null; // execute Application.ExitThread() on the STA thread Task.Factory.StartNew( () => Application.ExitThread(), CancellationToken.None, TaskCreationOptions.None, taskScheduler).Wait(); _thread.Join(); _thread = null; } } /// Task.Factory.StartNew wrappers public void Invoke(Action action) { Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait(); } public TResult Invoke(Func action) { return Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result; } public Task Run(Action action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler); } public Task Run(Func action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler); } public Task Run(Func action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap(); } public Task Run(Func> action, CancellationToken token) { return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap(); } } } 

Je soupçonne que l’exécution d’une boucle de traitement sur un autre thread ne fonctionnera pas bien, car WebBrowser est un composant d’interface utilisateur hébergeant un contrôle ActiveX.

Lorsque vous écrivez des wrappers TAP over EAP , je vous recommande d’utiliser des méthodes d’extension pour garder le code propre:

 public static Task NavigateAsync(this WebBrowser @this, ssortingng url) { var tcs = new TaskCompletionSource(); WebBrowserDocumentCompletedEventHandler subscription = null; subscription = (_, args) => { @this.DocumentCompleted -= subscription; tcs.TrySetResult(args.Url.ToSsortingng()); }; @this.DocumentCompleted += subscription; @this.Navigate(url); return tcs.Task; } 

Maintenant, votre code peut facilement appliquer un délai d’attente:

 async Task GetUrlAsync(ssortingng url) { using (var wb = new WebBrowser()) { var navigate = wb.NavigateAsync(url); var timeout = Task.Delay(TimeSpan.FromSeconds(5)); var completed = await Task.WhenAny(navigate, timeout); if (completed == navigate) return await navigate; return null; } } 

qui peuvent être consommés tels quels:

 private async Task GetFinalUrlAsync(PortalMerchant portalMerchant) { SetBrowserFeatureControl(); if (ssortingng.IsNullOrEmpty(portalMerchant.Url)) return null; var result = await GetUrlAsync(portalMerchant.Url); if (!Ssortingng.IsNullOrEmpty(result)) return new Uri(result); throw new Exception("Parsing Failed"); } 

J’essaie de profiter de la solution de Noseratio et des conseils de Stephen Cleary.

Voici le code que j’ai mis à jour pour inclure dans le code de Stephen le code de Noseratio concernant le conseil AJAX.

Première partie: la Task NavigateAsync conseillée par Stephen

 public static Task NavigateAsync(this WebBrowser @this, ssortingng url) { var tcs = new TaskCompletionSource(); WebBrowserDocumentCompletedEventHandler subscription = null; subscription = (_, args) => { @this.DocumentCompleted -= subscription; tcs.TrySetResult(args.Url.ToSsortingng()); }; @this.DocumentCompleted += subscription; @this.Navigate(url); return tcs.Task; } 

Deuxième partie: une nouvelle Task NavAjaxAsync pour exécuter le conseil pour AJAX (basé sur le code de Noseratio)

 public static async Task NavAjaxAsync(this WebBrowser @this) { // get the root element var documentElement = @this.Document.GetElementsByTagName("html")[0]; // poll the current HTML for changes asynchronosly var html = documentElement.OuterHtml; while (true) { // wait asynchronously await Task.Delay(POLL_DELAY); // continue polling if the WebBrowser is still busy if (webBrowser.IsBusy) continue; var htmlNow = documentElement.OuterHtml; if (html == htmlNow) break; // no changes detected, end the poll loop html = htmlNow; } return @this.Document.Url.ToSsortingng(); } 

Troisième partie: une nouvelle Task NavAndAjaxAsync pour obtenir la navigation et AJAX

 public static async Task NavAndAjaxAsync(this WebBrowser @this, ssortingng url) { await @this.NavigateAsync(url); await @this.NavAjaxAsync(); } 

Quasortingème et dernière partie: la Task GetUrlAsync mise à jour Task GetUrlAsync de Stephen avec le code de Noseratio pour AJAX

 async Task GetUrlAsync(ssortingng url) { using (var wb = new WebBrowser()) { var navigate = wb.NavAndAjaxAsync(url); var timeout = Task.Delay(TimeSpan.FromSeconds(5)); var completed = await Task.WhenAny(navigate, timeout); if (completed == navigate) return await navigate; return null; } } 

J’aimerais savoir si c’est la bonne approche.