Cómo cancelar Tarea esperando después de un período de tiempo de espera

Estoy utilizando este método para crear instancias de un navegador web mediante progtwigción, navegar a una URL y devolver un resultado cuando el documento se haya completado.

¿Cómo podría detener la Task y hacer que GetFinalUrl() devuelva null si el documento tarda más de 5 segundos en cargarse?

He visto muchos ejemplos usando TaskFactory pero no he podido aplicarlo a este código.

  private Uri GetFinalUrl(PortalMerchant portalMerchant) { SetBrowserFeatureControl(); Uri finalUri = null; if (string.IsNullOrEmpty(portalMerchant.Url)) { return null; } Uri trackingUrl = new Uri(portalMerchant.Url); var task = MessageLoopWorker.Run(DoWorkAsync, trackingUrl); task.Wait(); if (!String.IsNullOrEmpty(task.Result.ToString())) { return new Uri(task.Result.ToString()); } 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.ToString()); 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 } 

Actualizado : la última versión del WebBrowser web de la consola basada en WebBrowser se puede encontrar en Github .

Actualizado : Agregar un grupo de objetos WebBrowser para múltiples descargas paralelas.

¿Tiene un ejemplo de cómo hacer esto en una aplicación de consola por casualidad? Además, no creo que WebBrowser pueda ser una variable de clase porque estoy ejecutando todo en un paralelo para cada uno, iterando miles de URL

A continuación se muestra una implementación de WebBrowser web basado en WebBrowser más o menos genérico, que funciona como aplicación de consola. Es una consolidación de algunos de mis esfuerzos anteriores relacionados con WebBrowser , incluido el código al que se hace referencia en la pregunta:

  • Captura de una imagen de la página web con opacidad

  • Cargando una página con contenido dynamic de AJAX

  • Crear un hilo de bucle de mensaje STA para WebBrowser

  • Cargando un conjunto de URL, una tras otra

  • Imprimir un conjunto de URL con WebBrowser

  • La página web de automatización de UI

Algunos puntos:

  • La clase reutilizable MessageLoopApartment se utiliza para iniciar y ejecutar un hilo WinForms STA con su propia bomba de mensajes. Se puede usar desde una aplicación de consola , como se muestra a continuación. Esta clase expone un Progtwigdor de tareas TPL ( FromCurrentSynchronizationContext ) y un conjunto de contenedores Task.Factory.StartNew para usar este planificador de tareas.

  • Esto hace async/await una gran herramienta para ejecutar tareas de navegación WebBrowser en ese hilo STA separado. De esta forma, un objeto WebBrowser se crea, navega y destruye en ese hilo. Aunque, MessageLoopApartment no está vinculado específicamente a WebBrowser .

  • Es importante habilitar la representación de HTML5 mediante el Control de funciones del navegador , ya que de lo contrario, WebBrowser obejcts se ejecuta en modo de emulación IE7 de forma predeterminada. Eso es lo que hace SetFeatureBrowserEmulation continuación.

  • Puede que no siempre sea posible determinar cuándo una página web ha terminado de renderizar con un 100% de probabilidad. Algunas páginas son bastante complejas y usan actualizaciones continuas de AJAX. Sin embargo, podemos acercarnos bastante, al manejar primero el evento DocumentCompleted , luego al sondear la instantánea HTML actual de la página para ver los cambios y verificar la propiedad WebBrowser.IsBusy . Eso es lo que hace NavigateAsync continuación.

  • Una lógica de tiempo de espera está presente además de la anterior, en caso de que la representación de la página sea interminable (observe CancellationTokenSource y 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(string[] 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 string 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(string[] 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, string 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(); } } } 

Sospecho que ejecutar un ciclo de procesamiento en otro subproceso no funcionará bien, ya que WebBrowser es un componente de UI que aloja un control ActiveX.

Cuando está escribiendo TAP en contenedores EAP , le recomiendo usar métodos de extensión para mantener el código limpio:

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

Ahora su código puede aplicar fácilmente un tiempo de espera:

 async Task GetUrlAsync(string 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; } } 

que puede ser consumido como tal:

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

Intento aprovechar la solución de Noseratio y seguir los consejos de Stephen Cleary.

Aquí está el código que actualicé para incluir en el código de Stephen el código de Noseratio con respecto a la sugerencia de AJAX.

Primera parte: la Task NavigateAsync aconsejada por Stephen

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

Segunda parte: una nueva Task NavAjaxAsync para ejecutar la sugerencia para AJAX (basado en el código 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.ToString(); } 

Tercera parte: una nueva Task NavAndAjaxAsync para obtener la navegación y el AJAX

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

Cuarta y última parte: la Task GetUrlAsync actualizada de Stephen con el código de Noseratio para AJAX

 async Task GetUrlAsync(string 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; } } 

Me gustaría saber si este es el enfoque correcto.