Como cancelar a tarefa que aguarda após um período de tempo limite

Eu estou usando esse método para instanciar um navegador da Web por meio de programação, navegue até um URL e retorne um resultado quando o documento for concluído.

Como eu seria capaz de parar a Task e ter GetFinalUrl() retornar null se o documento leva mais de 5 segundos para carregar?

Já vi muitos exemplos usando um TaskFactory mas não consegui aplicá-lo a esse 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 } 

Atualizado : a versão mais recente do scrapper da web do console baseada no WebBrowser pode ser encontrada no Github .

Atualizado : Adicionando um pool de objects WebBrowser para vários downloads paralelos.

Você tem um exemplo de como fazer isso em um aplicativo de console por acaso? Também não acho que o webBrowser possa ser uma variável de class porque estou executando a coisa toda em uma parallell para cada, iterando milhares de URLs

Abaixo está uma implementação de um scrapper web mais ou menos genérico baseado no WebBrowser , que funciona como aplicativo de console. É uma consolidação de alguns dos meus esforços anteriores relacionados ao WebBrowser , incluindo o código mencionado na pergunta:

  • Capturando uma imagem da página da web com opacidade

  • Carregando uma página com conteúdo dynamic de AJAX

  • Criando um encadeamento de loop de mensagem STA para o WebBrowser

  • Como carregar um conjunto de URLs, um após o outro

  • Imprimir um conjunto de URLs com o WebBrowser

  • Automação da interface do usuário da página da Web

Alguns pontos:

  • A class MessageLoopApartment reutilizável é usada para iniciar e executar um thread STA do WinForms com sua própria message pump. Pode ser usado em um aplicativo de console , conforme abaixo. Esta class expõe um Agendador de Tarefas TPL ( FromCurrentSynchronizationContext ) e um conjunto de wrappers Task.Factory.StartNew para usar esse agendador de tarefas.

  • Isso torna async/await uma ótima ferramenta para executar tarefas de navegação do WebBrowser nesse thread STA separado. Dessa forma, um object WebBrowser é criado, navegado e destruído nesse segmento. Embora, MessageLoopApartment não esteja vinculado ao WebBrowser especificamente.

  • É importante ativar a renderização de HTML5 usando o Controle de Recurso do Navegador , caso contrário, os objects WebBrowser serão executados no modo de emulação do IE7 por padrão. Isso é o que SetFeatureBrowserEmulation faz abaixo.

  • Pode nem sempre ser possível determinar quando uma página da Web terminou a renderização com 100% de probabilidade. Algumas páginas são bastante complexas e usam atualizações contínuas do AJAX. No entanto, podemos nos aproximar bastante, manipulando o evento DocumentCompleted primeiro, depois pesquisando o instantâneo HTML atual da página para ver as alterações e verificando a propriedade WebBrowser.IsBusy . É o que o NavigateAsync faz abaixo.

  • Uma lógica de tempo limite está presente em cima do acima, caso a renderização da página seja interminável (note CancellationTokenSource e 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(); } } } 

Eu suspeito que executar um loop de processamento em outro thread não funcionará bem, uma vez que o WebBrowser é um componente de interface do usuário que hospeda um controle ActiveX.

Quando você está escrevendo TAP sobre wrappers EAP , recomendo usar methods de extensão para manter o código limpo:

 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; } 

Agora seu código pode facilmente aplicar um tempo limite:

 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 pode ser consumida 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"); } 

Estou tentando tirar proveito da solução da Noseratio, bem como seguindo os conselhos de Stephen Cleary.

Aqui está o código que atualizei para include no código do Stephen o código do Noseratio referente à dica AJAX.

Primeira parte: a Task NavigateAsync aconselhada 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: uma nova Task NavAjaxAsync para executar a dica para AJAX (com base no código da 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(); } 

Terceira parte: uma nova Task NavAndAjaxAsync para obter a navegação e o AJAX

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

Quarta e última parte: a Task GetUrlAsync atualizada Task GetUrlAsync de Stephen com o código da 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; } } 

Eu gostaria de saber se esta é a abordagem correta.