| |
|
1
|
Foros Generales / Dudas Generales / Re: Los links de la IA de Google ¡son larguísimos!
|
en: 23 Mayo 2026, 00:34 am
|
Por otro lado Elektro, ese link que pusiste no tiene un tiempo de validez?, es decir si pasado x tiempo por ejemplo días, semanas o meses, va a funcionar igualmente o dejará de funcionar?. No se presenta como un servicio con limitaciones ni fecha de caducidad configurable, por lo que, en principio, el enlace por sí solo no tendría por qué tener "tiempo de validez". Los enlaces se pueden gestionar desde la sección "Tus enlaces públicos" dentro de Gemini. Supongo que un enlace es permanente hasta que decidas eliminarlo manualmente. O hasta que decidas eliminar la conversación. En ese caso, el enlace además se borra de forma automática (lo acabo de comprobar), por lo que no hay que preocuparse de gestionar enlaces "muertos" que apuntan a conversaciones que han dejado de existir, por que eso ya lo hace Google automáticamente. Esto es lo que pone en la interfaz de la sección "Tus enlaces públicos" en Gemini: Tus enlaces estarán disponibles de forma pública mientras la conversación asociada esté guardada en el ajuste Actividad en las Aplicaciones de Gemini. Si alguna parte de una conversación se elimina, el enlace público asociado también se elimina.
A lo mejor podría existir algún límite "oculto" relacionado, como por ejemplo una cantidad máxima permitida de enlaces generados (¿100, 1.000, 2.000?), pero saberlo con certeza es complicado por la opacidad de Google. Habría que terminar leyendo textos densos sobre políticas de la plataforma y documentación del servicio / producto en específico, o documentación oficial de sus APIs para programación. A veces son cosas que están bastante ocultas en un rincón entre textos desplegables que nadie lee, como ese otro límite "oculto" y dinámico en el que nadie piensa respecto al límite de suscripciones a canales de Youtube que una persona puede tener y que se especifica de forma bastante escondida aquí: https://support.google.com/youtube/answer/4489286 en los textos desplegables de abajo (spoiler: son 2.000 canales, pero es un límite que "evoluciona", signifique lo que cojones signifique eso). Eso por poner como ejemplo lo que cuesta a veces averiguar fuentes oficiales que expliquen limitaciones técnicas de los productos y servicios de Google.
|
|
|
|
|
4
|
Foros Generales / Foro Libre / Re: TRUMP DESCLASIFICA OVNIS REALES ¡IMPRESIONANTE! 👽🛸
|
en: 18 Mayo 2026, 20:12 pm
|
¿Esa especie de cuerda es un adorno o correa como para perro? Veo que un extremo sale de una especie de pulsera, pero el otro no lo veo. ¿De verdad no lo asimilas?. Son grilletes, como los que lleva en los tobillos. La cadena que debería unir los grilletes de la muñecas, la IA simplemente lo hizo mal, y se quedó suspendida en el aire.
|
|
|
|
|
5
|
Foros Generales / Foro Libre / Re: TRUMP DESCLASIFICA OVNIS REALES ¡IMPRESIONANTE! 👽🛸
|
en: 17 Mayo 2026, 16:03 pm
|
nos están preparando poco a poco También llevan "preparándonos" con catástrofes sobre el calentamiento global enfriamiento global crisis climática emergencia climática cuento climático chiringuito corporativo impuesto al aire eco-ansiedad cambio climático desde el siglo XIX, cuando los periódicos empezaron a imprimirse masivamente y se vendían en la calle. Lo puedes buscar, hay muchos registros públicos en Internet que recopilan estas publicaciones sobre apocalipsis climáticos desde el siglo XIX hasta la actualidad... Siempre predicen un año, y un escenario catastrófico, generalmente la Antártida derretida completamente, o mencionando alguna isla que se hundirá bajo el agua, y así cada pocos años. Pero al final, sucede lo mismo que con el calendario Maya: Llega el año del apocalipsis, y no sucede nada, todo sigue igual de vivo. El problema es que la gente tiene memoria de pez:  Volviendo al tema, compañero Flamer (aunque todo esto que he explicado tiene mucho que ver para entender mejor el propósito), no te están preparando para un contacto alienígena, solo te están vendiendo el relato del miedo (humo), con el fin de mantenerte más manipulable cuando los políticos lo necesiten, ya que está muy demostrado que el alarmismo vende, y sirve para mantener manipulable a la población. Eso, y también para montar todo un negocio alrededor del cuento, claro está, el dinero es un motivo y motor igual de principal, sino más, que controlar a una sociedad o influir en su voluntad. Cuando se les agote un tema, es decir, cuando ya la mayoría de la gente cuestione un relato, entonces empezarán a impulsar fuertemente el alarmismo alienígena, pero solo será otro relato con el mismo objetivo que el anterior. ¡Un saludo!
|
|
|
|
|
6
|
Programación / Scripting / [APORTE] [PowerShell] Desactivar directivas de caché de escritura en todos los discos conectados.
|
en: 17 Mayo 2026, 15:41 pm
|
El siguiente script, desarrollado en PowerShell, sirve para desactivar las directivas de caché de escritura en todos los discos físicos actualmente conectados, para evitar que cada disco tenga una configuración distinta y asegurarse de que el comportamiento de escritura en disco sea coherente y seguro en todo el sistema, evitando riesgo de pérdida de datos o fallo del disco por un corte de luz. Y sí, uso la palabra evitar, y lo hago en modo afirmativo, ya que en más de 15 años con la caché desactivada y muchos cortes de luz (y un apagón en España) no he sufrido pérdida de datos ni fallos en ninguno de mis discos ni una sola vez. Antes de adquirir el hábito de desactivar la caché, sí tuve muchos problemas con cada corte de luz, pero después de adquirir el hábito, ni uno solo. Por ese motivo recomiendo encarecidamente mantener siempre desactivada la caché de escritura en todos los discos. El disco irá más lento, pero eso que pierdes lo ganas multiplicado en seguridad.  La primera casilla de arriba viene activada por defecto en Windows cuando se detecta un nuevo disco conectado.
El script se ha desarrollado mediante vibe coding con inteligencia artificial, y un poco de edición manual en el código resultante. Lo hice para un amigo y lo comparto tal cual.  #Requires -RunAsAdministrator Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # Disable both write-cache options on ALL connected disk drives # Option 1: Turn off write caching on the device > UserWriteCacheSetting = 0 # Option 2: Turn off Windows write-cache flushing > CacheIsPowerProtected = 0 [int] $successCount = 0 [int] $failCount = 0 Write-Host "" Write-Host "============================================================" -ForegroundColor Cyan Write-Host " Disable Write-Cache Options - All Physical Disks" -ForegroundColor Cyan Write-Host "============================================================" -ForegroundColor Cyan Write-Host "" [System.Object[]] $diskDevices = @( Get-PnpDevice -Class DiskDrive -Status OK -ErrorAction SilentlyContinue ) if ($diskDevices.Count -eq 0) { Write-Warning "No disk drives found with status OK." Write-Host "Press any key to exit..." $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") exit 1 } [System.Collections.Hashtable] $driveLetterMap = @{} [System.Collections.Hashtable] $diskSizeMap = @{} [System.Collections.Hashtable] $diskLabelMap = @{} function Format-DiskSize { param([uint64] $sizeBytes) if ($sizeBytes -ge 1TB) { return "$([math]::Round($sizeBytes / 1TB, 2)) TB" } elseif ($sizeBytes -ge 1GB) { return "$([math]::Round($sizeBytes / 1GB, 2)) GB" } else { return "$([math]::Round($sizeBytes / 1MB, 2)) MB" } } Get-CimInstance -ClassName Win32_LogicalDisk -ErrorAction SilentlyContinue | ForEach-Object { [string] $letter = $_.DeviceID [object] $diskDrive = $_ | Get-CimAssociatedInstance -ResultClassName Win32_DiskPartition -ErrorAction SilentlyContinue | Get-CimAssociatedInstance -ResultClassName Win32_DiskDrive -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $diskDrive) { [string] $pnpId = $diskDrive.PNPDeviceID.ToUpper() if (-not $driveLetterMap.ContainsKey($pnpId)) { $driveLetterMap[$pnpId] = $letter } if (-not $diskSizeMap.ContainsKey($pnpId)) { $diskSizeMap[$pnpId] = [uint64]$diskDrive.Size } if (-not $diskLabelMap.ContainsKey($pnpId)) { $diskLabelMap[$pnpId] = [string]$_.VolumeName } } } # Sort disk devices by their first drive letter; disks without letter go last [System.Object[]] $sortedDevices = @( $diskDevices | Sort-Object -Property { [string] $key = $_.InstanceId.ToUpper() if ($driveLetterMap.ContainsKey($key)) { $driveLetterMap[$key] } else { 'ZZ:' } } ) Write-Host "Found $($sortedDevices.Count) disk(s). Processing...`n" -ForegroundColor Yellow foreach ($device in $sortedDevices) { [string] $friendlyName = $device.FriendlyName [string] $instanceId = $device.InstanceId [string] $driveLetter = $driveLetterMap[$instanceId.ToUpper()] [string] $diskSize = Format-DiskSize -sizeBytes $diskSizeMap[$instanceId.ToUpper()] [string] $diskLabel = $diskLabelMap[$instanceId.ToUpper()] [string] $regPath = "HKLM:\SYSTEM\CurrentControlSet\Enum\$instanceId\Device Parameters\Disk" Write-Host "-----------------------------------------------------" -ForegroundColor DarkGray Write-Host " Disk : [$driveLetter] $diskLabel - $friendlyName ($diskSize)" -ForegroundColor White Write-Host " ID : $instanceId" -ForegroundColor DarkGray if (-not (Test-Path -Path $regPath)) { Write-Warning " Registry path not found - skipping: $regPath" $failCount++ continue } # Option 1: Disable write caching # UserWriteCacheSetting: # 0 = System default | 1 = Force ENABLE | 2 = Force DISABLE try { Set-ItemProperty -Path $regPath ` -Name "UserWriteCacheSetting" ` -Value 0 ` -Type DWord ` -Force Write-Host " [OK] Enable write caching DISABLED (UserWriteCacheSetting = 0)" -ForegroundColor Green } catch { Write-Warning " [FAIL] UserWriteCacheSetting - $_" $failCount++ } # Option 2: Re-enable buffer flushing (uncheck "turn off flushing") # CacheIsPowerProtected: # 0 = Flushing ENABLED (checkbox unchecked - safe mode) # 1 = Flushing DISABLED (checkbox checked - risky, power-loss danger) try { Set-ItemProperty -Path $regPath ` -Name "CacheIsPowerProtected" ` -Value 0 ` -Type DWord ` -Force Write-Host " [OK] Turn off write-cache buffer flushing DISABLED (CacheIsPowerProtected = 0)" -ForegroundColor Green $successCount++ } catch { Write-Warning " [FAIL] CacheIsPowerProtected - $_" $failCount++ } } # Summary Write-Host "" Write-Host "============================================================" -ForegroundColor Cyan Write-Host " Summary" -ForegroundColor Cyan Write-Host "============================================================" -ForegroundColor Cyan Write-Host " Disks processed successfully : $successCount" -ForegroundColor Green if ($failCount -gt 0) { Write-Host " Disks with errors : $failCount" -ForegroundColor Red } Write-Host "" Write-Host " NOTE: A system RESTART is required for changes" -ForegroundColor Yellow Write-Host " to take effect on all devices." -ForegroundColor Yellow Write-Host "" Write-Host "Press any key to exit..." $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
|
|
|
7
|
Programación / .NET (C#, VB.NET, ASP) / Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
|
en: 14 Mayo 2026, 17:46 pm
|
Por último, les muestro tres métodos de extensión: WaitForPageReady, WaitForElement e IsCloudflareChallengeRequired, para usar con la interfaz IWebDriver de Selenium, implementada en el tipo ChromeDriver. Loss tres muy útiles, y para mi son de uso muy común. WaitForPageReady y WaitForElement encapsulan patrones habituales de espera y validación que normalmente se terminan reescribiendo en múltiples proyectos, centralizando la lógica en una capa reutilizable y consistente. WaitForPageReady permite esperar a que la página web alcance el estado de carga completa (document.readyState = "complete"), asegurando que el navegador ha finalizado la carga inicial del documento HTML. Adicionalmente, incorpora mecanismos opcionales para esperar un tiempo adicional tras la carga inicial y detectar estabilidad del DOM (evitando cambios asíncronos posteriores). WaitForElement implementa un patrón de espera explícita para elementos del DOM. Su funcionalidad es simple pero crítica: Espera hasta que un elemento exista en el DOM, verifica que esté visible (Displayed), verifica que sea interactuable (Enabled), y devuelve directamente el elemento listo para su uso. La tercerca extensión, IsCloudflareChallengeRequired, permite detectar si la página actual está bloqueada por un desafío de Cloudflare. Este tipo de protección puede impedir completamente la automatización web si no se detecta correctamente, por lo que esta función actúa como una capa de diagnóstico temprano. Nota: Puede que mi implementación de detección no sea sofisticada, es realmente una heurística muy simple basada en validación de texto, la cual ha sido eficaz 100% en todos los escenarios que he probado, pero no sé si abarcará escenarios variantes que puedan ocasionar las páginas protegidas por Cloudflare. Módulo IWebDriverExtensions: #Region " Option Statements " Option Strict On Option Explicit On Option Infer Off #End Region #Region " Imports " Imports OpenQA.Selenium Imports OpenQA.Selenium.Internal Imports OpenQA.Selenium.Support.UI #End Region #Region " IWebDriver Extensions " ' ReSharper disable once CheckNamespace 'Namespace DevCase.ThirdParty.Selenium.Extensions.IWebDriverExtensions ''' <summary> ''' Provides extension methods to use with the <see cref="IWebDriver"/> interface. ''' </summary> ''' ''' <remarks> ''' Note: Some functionalities of this assembly may require to install one or all of the listed NuGet packages: ''' <para></para> ''' <see href="https://www.nuget.org/packages/Selenium.Support">Selenium.Support by Selenium Committers</see> ''' <para></para> ''' <see href="https://www.nuget.org/packages/Selenium.WebDriver">Selenium.WebDriver by Selenium Committers</see> ''' <para></para> ''' </remarks> <HideModuleName> Public Module IWebDriverExtensions #Region " Public Extension Methods " ''' <summary> ''' Waits for the current web page in the specified <see cref="IWebDriver"/> instance ''' to report a ready state of <c>"complete"</c>. And optionally, it can also ''' wait for any pending dynamic updates in the DOM to complete after the page ''' has reported a ready state of <c>"complete"</c>. ''' </summary> ''' ''' <remarks> ''' Note: Some functionalities of this assembly may require to install one or all of the listed NuGet packages: ''' <para></para> ''' <see href="https://www.nuget.org/packages/Selenium.Support">Selenium.Support by Selenium Committers</see> ''' <para></para> ''' <see href="https://www.nuget.org/packages/Selenium.WebDriver">Selenium.WebDriver by Selenium Committers</see> ''' <para></para> ''' </remarks> ''' ''' <param name="driver"> ''' The <see cref="IWebDriver"/> instance. ''' </param> ''' ''' <param name="afterPageReadyDelay"> ''' Optional. A <see cref="TimeSpan"/> representing the delay to wait ''' <b>after</b> the web page reports a ready state of <c>"complete"</c>, ''' before the method returns. ''' <para></para> ''' This can be useful to allow background scripts, animations, or ''' asynchronous content to finish initializing after the document is loaded. ''' <para></para> ''' Default value is null. ''' </param> ''' ''' <param name="waitForDomIdle"> ''' Optional. When set to <see langword="True"/>, the method starts waiting for any pending dynamic updates in the DOM ''' to complete after the page has reported a ready state of <c>"complete"</c>. ''' <para></para> ''' Default value is <see langword="False"/>. ''' <para></para> ''' ⚠️ Do not set this parameter to <see langword="True"/> for web pages with continuously changing DOM elements ''' (e.g., pages with animations, snow effects, or real-time updates). ''' </param> ''' ''' <param name="timeoutSeconds"> ''' Optional. The maximum time in seconds to wait for the page to report a ready state of <c>"complete"</c>, ''' and for any pending dynamic updates in the DOM to complete if ''' <paramref name="waitForDomIdle"/> is set to <see langword="True"/>. ''' <para></para> ''' Default value is 30 seconds. ''' <para></para> ''' If the condition is not met within this time, a <see cref="WebDriverTimeoutException"/> is thrown. ''' </param> ''' ''' <param name="throwOnTimeout"> ''' Optional. When set to <see langword="True"/>, ''' a <see cref="WebDriverTimeoutException"/> will be thrown if the time specified in ''' <paramref name="timeoutSeconds"/> parameter reaches while waiting the ''' web page to report a ready state of <c>"complete"</c>, ''' or while waiting for any pending dynamic updates in the DOM to complete after the ''' page has reported a ready state of <c>"complete"</c>. ''' <para></para> ''' Default value is <see langword="True"/>. ''' </param> <DebuggerStepThrough> <Extension> <EditorBrowsable(EditorBrowsableState.Always)> Public Sub WaitForPageReady(driver As IWebDriver, Optional afterPageReadyDelay As TimeSpan = Nothing, Optional waitForDomIdle As Boolean = False, Optional timeoutSeconds As Integer = 30, Optional throwOnTimeout As Boolean = True) If timeoutSeconds <= 0 Then Throw New ArgumentException("Timeout must be a value greater than zero.", NameOf(timeoutSeconds)) End If Dim js As IJavaScriptExecutor = TryCast(driver, IJavaScriptExecutor) If js Is Nothing Then Throw New ArgumentException("Driver must support javascript execution", NameOf(driver)) End If Dim wait As New WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds)) With { .PollingInterval = TimeSpan.FromMilliseconds(500) } Dim startTime As Date = Date.Now Dim domLength As Integer wait.Until( Function(d As IWebDriver) Try Dim readyState As String = js.ExecuteScript("if (document.readyState) return document.readyState;").ToString() If readyState.Equals("complete", StringComparison.OrdinalIgnoreCase) Then domLength = d.PageSource.Length Return True Else Return False End If Catch ex As WebDriverTimeoutException If throwOnTimeout Then Throw End If Return True Catch ex As InvalidOperationException ' Window is no longer available #If Not NETCOREAPP Then Return ex.Message.IndexOf("unable to get browser", StringComparison.OrdinalIgnoreCase) >= 0 #Else Return ex.Message.Contains("unable to get browser", StringComparison.OrdinalIgnoreCase) #End If Catch ex As WebDriverException ' Browser is no longer available #If Not NETCOREAPP Then Return ex.Message.IndexOf("unable to connect", StringComparison.OrdinalIgnoreCase) >= 0 #Else Return ex.Message.Contains("unable to connect", StringComparison.OrdinalIgnoreCase) #End If Catch ex As Exception Return True End Try End Function) If afterPageReadyDelay <> Nothing Then Thread.Sleep(afterPageReadyDelay) End If ' Even when "document.readyState()" returns "complete", web pages can continue to modify ' the DOM dynamically after the initial load. This can occur due to asynchronous scripts, ' client-side rendering frameworks (such as React, Angular, or Vue), AJAX/fetch requests ' that inject additional content and rewrites portions of the page, etc. ' ' As a result, the page source may still change for a short period of time even though the ' web browser reports that the document has finished loading. ' ' This check ensures that the HTML content remains stable (IDLE) before exiting. If waitForDomIdle Then Dim newDomLength As Integer = driver.PageSource.Length If newDomLength <> domLength Then Dim elapsedTime As TimeSpan = Date.Now - startTime timeoutSeconds -= CInt(elapsedTime.TotalSeconds) If timeoutSeconds <= 0 Then timeoutSeconds = 1 End If Dim domWait As New WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds)) With { .PollingInterval = TimeSpan.FromSeconds(1) } domWait.Until(Function(d As IWebDriver) Try Dim length As Integer = d.PageSource.Length Dim idle As Boolean = (length = newDomLength) newDomLength = length Return idle Catch ex As WebDriverTimeoutException If throwOnTimeout Then Throw End If Return True Catch ex As InvalidOperationException ' Window is no longer available #If Not NETCOREAPP Then Return ex.Message.IndexOf("unable to get browser", StringComparison.OrdinalIgnoreCase) >= 0 #Else Return ex.Message.Contains("unable to get browser", StringComparison.OrdinalIgnoreCase) #End If Catch ex As WebDriverException ' Browser is no longer available #If Not NETCOREAPP Then Return ex.Message.IndexOf("unable to connect", StringComparison.OrdinalIgnoreCase) >= 0 #Else Return ex.Message.Contains("unable to connect", StringComparison.OrdinalIgnoreCase) #End If Catch ex As Exception Return True End Try End Function) End If End If End Sub ''' <summary> ''' Waits until an element matching the specified <see cref="By"/> selector is present ''' in the DOM of the specified <see cref="IWebDriver"/>. ''' </summary> ''' ''' <param name="driver"> ''' The <see cref="IWebDriver"/> instance. ''' </param> ''' ''' <param name="by"> ''' The <see cref="By"/> selector used to locate the element. ''' </param> ''' ''' <param name="timeoutSeconds"> ''' Optional. The maximum number of seconds to wait. Default is 30 seconds. ''' <para></para> ''' If the condition is not met within this time, a <see cref="WebDriverTimeoutException"/> is thrown. ''' </param> ''' ''' <returns> ''' If the function succeds, returns the found <see cref="IWebElement"/>. ''' </returns> <Extension> <DebuggerStepThrough> <EditorBrowsable(EditorBrowsableState.Always)> Public Function WaitForElement(driver As IWebDriver, by As By, Optional timeoutSeconds As Integer = 30) As IWebElement If timeoutSeconds <= 0 Then Throw New ArgumentException("Timeout must be a value greater than zero.", NameOf(timeoutSeconds)) End If Dim wait As New WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds)) With { .PollingInterval = TimeSpan.FromMilliseconds(250) } Return wait.Until(Function(d As IWebDriver) Try Dim element As IWebElement = d.FindElement(by) ' Check if element is displayed and enabled (interactable). If (element IsNot Nothing) AndAlso element.Displayed AndAlso element.Enabled Then Return element End If Catch ex As NoSuchElementException ' Ignore. End Try ' Return Nothing to continue waiting until timeout. Return Nothing End Function) End Function ''' <summary> ''' Determines whether the current web page loaded in the specified <see cref="IWebDriver"/> is protected by a Cloudflare challenge, ''' so a navigation block or anti-bot challenge is currently being displayed instead of the expected content. ''' </summary> ''' ''' <param name="driver"> ''' The <see cref="IWebDriver"/> instance. ''' </param> ''' ''' <returns> ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page; ''' otherwise, <see langword="False"/>. ''' </returns> <Extension> <DebuggerStepThrough> Public Function IsCloudflareChallengeRequired(driver As IWebDriver) As Boolean #If NETCOREAPP Then ArgumentNullException.ThrowIfNull(driver) #Else If driver Is Nothing Then Throw New ArgumentNullException(NameOf(driver)) End If #End If Dim pageSource As String = driver.PageSource Dim pageTitle As String = driver.Title Return UtilWeb.IsCloudflareChallengeRequired(pageSource, pageTitle) End Function #End Region End Module 'End Namespace #End Region
Clase adicional UtilWeb, necesaria para poder usar la extensión IsCloudflareChallengeRequired (del módulo IWebDriverExtensions compartido aquí arriba): #Region " Option Statements " Option Strict On Option Explicit On Option Infer Off #End Region #Region " Imports " #End Region #Region " Web Util " ' ReSharper disable once CheckNamespace 'Namespace DevCase.Core.Networking.Common ''' <summary> ''' Provides web-related utility functions. ''' </summary> Public NotInheritable Class UtilWeb #Region " Constructors " ''' <summary> ''' Prevents a default instance of the <see cref="UtilWeb"/> class from being created. ''' </summary> <DebuggerNonUserCode> Private Sub New() End Sub #End Region #Region " Public Methods " ''' <summary> ''' Determines whether the provided HTML source-code indicates that a Cloudflare challenge is required to access the page. ''' </summary> ''' ''' <param name="pageSource"> ''' The raw HTML source code. ''' </param> ''' ''' <param name="pageTitle"> ''' Optional. The title of the web page. ''' </param> ''' ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page; ''' otherwise, <see langword="False"/>. <DebuggerStepThrough> Public Shared Function IsCloudflareChallengeRequired(pageSource As String, pageTitle As String) As Boolean If String.IsNullOrWhiteSpace(pageSource) Then Return False End If Dim challengeIndicators As String() = { "challenge-error-text", "/cdn-cgi/challenge-platform", "window._cf_chl_opt", "<title>Just a moment...</title>" } For Each indicator As String In challengeIndicators #If NETCOREAPP Then If pageSource.Contains(indicator, StringComparison.OrdinalIgnoreCase) Then Return True End If #Else If pageSource.IndexOf(indicator, StringComparison.OrdinalIgnoreCase) >= 0 Then Return True End If #End If Next Return String.Equals(pageTitle, "Just a moment...", StringComparison.OrdinalIgnoreCase) End Function ' ''' <summary> ' ''' Sends an HTTP request to the specified URL to determine whether ' ''' a Cloudflare challenge is required to load the web page that points to. ' ''' </summary> ' ''' ' ''' <param name="url"> ' ''' The URL to check. ' ''' </param> ' ''' ' ''' <returns> ' ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page; ' ''' otherwise, <see langword="False"/>. ' ''' </returns> ' <DebuggerStepThrough> ' Public Shared Function IsCloudflareChallengeRequired(url As String) As Boolean ' ' Using handler As New HttpClientHandler() With { ' .AllowAutoRedirect = True, ' .AutomaticDecompression = DecompressionMethods.GZip Or DecompressionMethods.Deflate ' } ' ' Using client As New HttpClient(handler) ' Dim resp As HttpResponseMessage = client.GetAsync(url).ConfigureAwait(False).GetAwaiter().GetResult() ' Dim pageSource As String = resp.Content.ReadAsStringAsync().ConfigureAwait(False).GetAwaiter().GetResult() ' ' Return (resp.StatusCode <> HttpStatusCode.OK) AndAlso ' UtilWeb.IsCloudflareChallengeRequired(pageSource, pageTitle:=Nothing) ' End Using ' End Using ' End Function ' ' ''' <summary> ' ''' Sends an HTTP request to the specified <see cref="Uri"/> to determine whether ' ''' a Cloudflare challenge is required to load the web page that points to. ' ''' </summary> ' ''' ' ''' <param name="uri"> ' ''' The <see cref="Uri"/> to check. ' ''' </param> ' ''' ' ''' <returns> ' ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page; ' ''' otherwise, <see langword="False"/>. ' ''' </returns> ' <DebuggerStepThrough> ' Public Shared Function IsCloudflareChallengeRequired(uri As Uri) As Boolean ' ' Return UtilWeb.IsCloudflareChallengeRequired(uri.ToString()) ' End Function #End Region End Class 'End Namespace #End Region
|
|
|
|
|
8
|
Programación / .NET (C#, VB.NET, ASP) / Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
|
en: 14 Mayo 2026, 17:16 pm
|
Ahora les muestro la clase ConsoleTerminationWatcher, generada por IA (la comparto tal cual, sin ninguna refactorización ni organización de código por mi parte... más allá de haberle añadido algo de documentación inicial), cuyo propósito es interceptar los eventos de cierre y terminación de una aplicación de consola para ejecutar una lógica arbitraria de limpieza, de forma segura y controlada. 💡 Tip: Esta clase es el complemento ideal para combinar con el uso del método KillDriverAndChildBrowsers que compartí en el post anterior a este, sobre la automatización con Selenium. Ejemplo de uso generado por IA: Imports System Imports System.Threading Module Program ' Declared outside Main to keep it alive during the entire process lifetime Private _watcher As ConsoleTerminationWatcher Sub Main() Console.WriteLine("Application started.") Console.WriteLine("Press CTRL + C or close the console window to trigger termination handler.") Console.WriteLine() _watcher = New ConsoleTerminationWatcher( Sub() Console.WriteLine() Console.WriteLine("Termination event captured!") Try Console.WriteLine("Executing cleanup logic...") ' Example: KillDriverAndChildBrowsers("chromedriver") ' Generic cleanup simulation Thread.Sleep(1000) Console.WriteLine("Cleanup finished successfully.") Catch ex As Exception Console.WriteLine($"Error during cleanup: {ex.Message}") End Try End Sub) ' Simulated long-running process Dim counter As Integer = 0 While True counter += 1 Console.WriteLine($"Running iteration {counter}") Thread.Sleep(1500) End While End Sub End Module
La clase ConsoleTerminationWatcher: Imports System.ComponentModel Imports System.Diagnostics Imports System.Runtime.InteropServices ''' <summary> ''' Provides a wrapper for the Win32 SetConsoleCtrlHandler function. ''' <para></para> ''' This class ensures that cleanup logic is executed when the console window is closed or interrupted. ''' </summary> Public Class ConsoleTerminationWatcher : Implements IDisposable ''' <summary> ''' Adds or removes an application-defined HandlerRoutine function from the list of handler functions for the calling process. ''' </summary> ''' ''' <param name="handler"> ''' A pointer to the application-defined HandlerRoutine function to be added or removed. ''' </param> ''' ''' <param name="add"> ''' If this parameter is TRUE, the handler is added; if it is FALSE, the handler is removed. ''' </param> ''' ''' <returns> ''' Returns TRUE if the function succeeds; otherwise, FALSE. ''' </returns> <DllImport("kernel32.dll", SetLastError:=True)> Private Shared Function SetConsoleCtrlHandler(handler As ConsoleEventDelegate, add As Boolean) As Boolean End Function ''' <summary> ''' An application-defined function used with the SetConsoleCtrlHandler function. ''' </summary> ''' ''' <param name="eventType"> ''' The type of control signal received by the handler. ''' </param> ''' ''' <returns> ''' If the function handles the control signal, it should return TRUE. ''' <para></para> ''' If it returns FALSE, the next handler function in the list is called. ''' </returns> Private Delegate Function ConsoleEventDelegate(eventType As ConsoleEventType) As Boolean ''' <summary> ''' Enumerates the control signal types received by the console control handler. ''' </summary> Private Enum ConsoleEventType As Integer ''' <summary> ''' A CTRL+C signal was received. ''' </summary> CTRL_C_EVENT = 0 ''' <summary> ''' A CTRL+BREAK signal was received. ''' </summary> CTRL_BREAK_EVENT = 1 ''' <summary> ''' The user closed the console window. ''' </summary> CTRL_CLOSE_EVENT = 2 ''' <summary> ''' The user is logging off. ''' </summary> CTRL_LOGOFF_EVENT = 5 ''' <summary> ''' The system is shutting down. ''' </summary> CTRL_SHUTDOWN_EVENT = 6 End Enum ''' <summary> ''' Holds the delegate reference to prevent it from being collected by the Garbage Collector. ''' </summary> Private ReadOnly _handler As ConsoleEventDelegate ''' <summary> ''' The action to be executed when a termination event occurs. ''' </summary> Private ReadOnly _cleanupAction As Action ''' <summary> ''' Tracks the disposal status of the instance. ''' </summary> Private _disposedValue As Boolean = False ''' <summary> ''' Initializes a new instance of the <see cref="ConsoleTerminationWatcher"/> class. ''' </summary> ''' ''' <param name="cleanupAction"> ''' The action to execute when the console terminates (e.g., process cleanup). ''' </param> ''' ''' <exception cref="ArgumentNullException"> ''' Thrown when cleanupAction is null. ''' </exception> ''' ''' <exception cref="Win32Exception"> ''' Thrown when the Win32 API registration fails. ''' </exception> <DebuggerStepThrough> Public Sub New(cleanupAction As Action) If cleanupAction Is Nothing Then Throw New ArgumentNullException(NameOf(cleanupAction)) End If Me._cleanupAction = cleanupAction Me._handler = New ConsoleEventDelegate(AddressOf Me.ConsoleEventCallback) If Not SetConsoleCtrlHandler(Me._handler, True) Then Dim errorCode As Integer = Marshal.GetLastWin32Error() Throw New Win32Exception(errorCode, $"Failed to register console control handler. Win32 Error Code: {errorCode}") End If End Sub ''' <summary> ''' The callback method invoked by the Windows Operating System when a console event occurs. ''' </summary> ''' ''' <param name="eventType"> ''' The type of console event triggered. ''' </param> ''' ''' <returns> ''' Always returns FALSE to allow the process to terminate normally after cleanup. ''' </returns> <DebuggerStepThrough> Private Function ConsoleEventCallback(eventType As ConsoleEventType) As Boolean Select Case eventType Case ConsoleEventType.CTRL_C_EVENT, ConsoleEventType.CTRL_BREAK_EVENT, ConsoleEventType.CTRL_CLOSE_EVENT, ConsoleEventType.CTRL_LOGOFF_EVENT, ConsoleEventType.CTRL_SHUTDOWN_EVENT ' Trigger the external cleanup logic Me._cleanupAction.Invoke() Case Else ' Ignore other events. End Select Return False End Function ''' <summary> ''' Releases the resources used by the <see cref="ConsoleTerminationWatcher"/>. ''' </summary> ''' ''' <param name="disposing"> ''' True to release both managed and unmanaged resources; False to release only unmanaged resources. ''' </param> <DebuggerStepThrough> Protected Overridable Sub Dispose(disposing As Boolean) If Not Me._disposedValue Then If disposing Then ' Free managed objects here if any. End If ' Unregister the Win32 handler (Unmanaged resource cleanup) ' This must happen even if disposing is False (called from Finalizer) SetConsoleCtrlHandler(Me._handler, False) Me._disposedValue = True End If End Sub ''' <summary> ''' Finalizes an instance of the <see cref="ConsoleTerminationWatcher"/> class. ''' </summary> <DebuggerStepThrough> Protected Overrides Sub Finalize() ' Ensure the Win32 hook is removed if the programmer forgot to call Dispose Me.Dispose(disposing:=False) MyBase.Finalize() End Sub ''' <summary> ''' Releases the resources used by the <see cref="ConsoleTerminationWatcher"/>. ''' </summary> <DebuggerStepThrough> Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(disposing:=True) GC.SuppressFinalize(Me) End Sub End Class
|
|
|
|
|
9
|
Programación / .NET (C#, VB.NET, ASP) / Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
|
en: 14 Mayo 2026, 17:00 pm
|
Siguiendo con las herramientas generales para automatización mediante Selenium, aquí les dejo una función por nombre CreateOptimizedChromeDriver, que sirve para crear una instancia de ChromeDriver preconfigurada y optimizada para tareas de automatización web. La función provee varios parámetros opcionales de personalización de rutas, la posibilidad de añadir argumentos opcionales durante la creación de la instancia, y, además, poder ocultar la ventana de Chrome cuando se trabaja con interfaz gráfica / modo non-headless. La instancia creada por esta función del proceso Chrome con interfaz gráfica / modo non-headless (con la ventana visible u oculta), no debería dar problemas de ningún tipo al intentar completar los desafíos de Cloudflare. Utilizo esta función en numerosos proyectos de scraping. Ejemplo de uso: Dim driverService As ChromeDriverService = Nothing Dim headless As Boolean = False Dim hideNonHeadlessWindow As Boolean = False Dim driverFilePath As String = Nothing ' Optional, as the function automatically resolves the latest cached ChromeDriver.exe file path. Dim driverLogFilePath As String = Nothing ' Optional, as the function automatically resolves a default log file path if no specified. Dim chromeFilePath As String = Nothing ' Optional, as the function automatically resolves the latest cached Chrome.exe file path. Dim userDataDir As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache\chrome user data") Dim profileName As String = "Automation Profile" Dim additionalArguments As String() = {"--window-position=0,0"} Try Using driver As ChromeDriver = CreateOptimizedChromeDriver( driverService, driverFilePath:=driverFilePath, driverLogFilePath:=driverLogFilePath, chromeFilePath:=chromeFilePath, userDataDir:=userDataDir, profileName:=profileName, headless:=headless, hideNonHeadlessWindow:=hideNonHeadlessWindow, additionalArguments) driver.Navigate().GoToUrl("https://www.google.com/") Console.WriteLine($"Page Title: {driver.Title}") End Using Finally driverService?.Dispose() End Try
La función CreateOptimizedChromeDriver: ''' <summary> ''' Initializes and returns a <see cref="ChromeDriver"/> instance ''' with a preconfigured set of options optimized for browser automation, ''' and optionally hiding the Chrome window if not running in headless mode and specified by the caller. ''' </summary> ''' ''' <example> This is a code example. ''' <code language="VB"> ''' Dim driverService As ChromeDriverService = Nothing ''' Dim headless As Boolean = False ''' Dim hideNonHeadlessWindow As Boolean = False ''' Dim driverFilePath As String = Nothing ' Optional, as the function automatically resolves the latest cached ChromeDriver.exe file path. ''' Dim driverLogFilePath As String = Nothing ' Optional, as the function automatically resolves a default log file path if no specified. ''' Dim chromeFilePath As String = Nothing ' Optional, as the function automatically resolves the latest cached Chrome.exe file path. ''' Dim userDataDir As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cache\Chrome") ''' Dim profileName As String = "AutomationProfile" ''' Dim additionalArguments As String() = {"--window-position=-0,0"} ''' ''' Try ''' Using driver As ChromeDriver = CreateOptimizedChromeDriver( ''' driverService, ''' driverFilePath:=driverFilePath, ''' driverLogFilePath:=driverLogFilePath, ''' chromeFilePath:=chromeFilePath, ''' userDataDir:=userDataDir, ''' profileName:=profileName, ''' headless:=headless, ''' hideNonHeadlessWindow:=hideNonHeadlessWindow, ''' additionalArguments) ''' ''' driver.Navigate().GoToUrl("https://www.google.com/") ''' Console.WriteLine($"Page Title: {driver.Title}") ''' End Using ''' Finally ''' driverService?.Dispose() ''' End Try ''' </code> ''' </example> ''' ''' <param name="refDriverService"> ''' When this function returns, receives the <see cref="ChromeDriverService"/> instance created. ''' </param> ''' ''' <param name="driverFilePath"> ''' File path to ChromeDriver.exe; For example, ".\chromedriver.exe" ''' <para></para> ''' This value can be null, in which case the function will attempt to resolve the latest cached ChromeDriver.exe file path ''' from the Selenium cache directory (as configured in <c>SE_CACHE_PATH</c> environment variable) ''' using the <see cref="UtilSelenium.GetLatestCachedChromeDriverFilePath"/> function. ''' </param> ''' ''' <param name="driverLogFilePath"> ''' Full path to the log file where ChromeDriver logs will be written. For example, ".\chromedriver.log" ''' <para></para> ''' This value can be null, in which case a default log file path is used in the same directory as the ''' <paramref name="userDataDir"/> dierctory path, with the name "chromedriver.log". ''' </param> ''' ''' <param name="chromeFilePath"> ''' File path to Chrome.exe; For example, "C:\Program Files\Google Chrome\chrome.exe" ''' <para></para> ''' This value can be null, in which case the function will attempt to resolve the latest cached Chrome.exe file path ''' from the Selenium cache directory (as configured in <c>SE_CACHE_PATH</c> environment variable) ''' using the <see cref="UtilSelenium.GetLatestCachedChromeFilePath"/> function. ''' </param> ''' ''' <param name="userDataDir"> ''' The directory path for Chrome's user data. For example, ".\Cache\Chrome User Data" ''' <para></para> ''' This directory is used to store user-specific settings and data. ''' <para></para> ''' This value can be null, in which case the function will attempt to resolve the ''' Selenium cache directory (as configured in <c>SE_CACHE_PATH</c> environment variable) ''' and create a subdirectory inside it for Chrome user data, with the name "chrome user data". ''' If the Selenium cache directory cannot be resolved, the function will create the ''' user data directory inside the current application base directory, with the name "chrome user data". ''' </param> ''' ''' <param name="profileName"> ''' The name of the Chrome profile directory to use within the <paramref name="userDataDir"/>. ''' For example, "Default" or "Profile 1". ''' <para></para> ''' This value can be null, in which case "Default" profile name is assumed. ''' </param> ''' ''' <param name="headless"> ''' If <see langword="True"/>, launches Chrome in headless mode. ''' </param> ''' ''' <param name="hideNonHeadlessWindow"> ''' If <see langword="True"/> and <paramref name="headless"/> is set to <see langword="False"/>, ''' attempts to hide the Chrome window and taskbar icon. ''' <para></para> ''' Has no effect if <paramref name="headless"/> is set to <see langword="True"/>, ''' since Chrome windows are not shown in headless mode. ''' </param> ''' ''' <param name="additionalArguments"> ''' Optional. Additional arguments to add to the underlying <see cref="ChromeOptions"/> object ''' through its <see cref="ChromeOptions.AddArguments"/> method. ''' <para></para> ''' For example, when not running in headless mode you can use ''' <c>"--window-position=-32000,0"</c> to set the Chrome window off-screen, ''' or <c>"--window-size=1200,800"</c> to set the initial Chrome window size. ''' </param> ''' ''' <returns> ''' The resulting <see cref="ChromeDriver"/> instance. ''' </returns> <DebuggerStepThrough> Public Shared Function CreateOptimizedChromeDriver(ByRef refDriverService As ChromeDriverService, driverFilePath As String, driverLogFilePath As String, chromeFilePath As String, userDataDir As String, profileName As String, headless As Boolean, hideNonHeadlessWindow As Boolean, ParamArray additionalArguments As String()) As ChromeDriver If String.IsNullOrWhiteSpace(driverFilePath) Then Try driverFilePath = UtilSelenium.GetLatestCachedChromeDriverFilePath() Catch ex As Exception Throw New InvalidOperationException($"Failed to resolve ChromeDriver.exe file path from Selenium cache. See inner exception for details.", ex) End Try ElseIf Not String. IsNullOrWhiteSpace(driverFilePath ) AndAlso Not File. Exists(driverFilePath ) Then Throw New FileNotFoundException($"ChromeDriver.exe not found at the specified path: {driverFilePath}", driverFilePath) End If Dim driverDirPath As String = Path.GetDirectoryName(driverFilePath) Dim driveFileName As String = Path.GetFileName(driverFilePath) If String.IsNullOrWhiteSpace(chromeFilePath) Then Try chromeFilePath = UtilSelenium.GetLatestCachedChromeFilePath() Catch ex As Exception Throw New InvalidOperationException($"Failed to resolve Chrome.exe file path from Selenium cache. See inner exception for details.", ex) End Try ElseIf Not String. IsNullOrWhiteSpace(chromeFilePath ) AndAlso Not File. Exists(chromeFilePath ) Then Throw New FileNotFoundException($"Chrome.exe not found at the specified path: {chromeFilePath}", chromeFilePath) End If If String.IsNullOrWhiteSpace(userDataDir) Then Dim seCachePathEnvVar As String = Environment.GetEnvironmentVariable("SE_CACHE_PATH", EnvironmentVariableTarget.Process) If Not String.IsNullOrWhiteSpace(seCachePathEnvVar) Then Dim ass As Assembly = Assembly.GetEntryAssembly() Dim assName As String = ass.GetName().Name userDataDir = Path.Combine(seCachePathEnvVar, $"chrome user data - {assName}") Else userDataDir = Path.Combine(AppContext.BaseDirectory, "chrome user data") End If End If userDataDir = Path.GetFullPath(userDataDir) If String.IsNullOrWhiteSpace(profileName) Then profileName = "Default" End If If String.IsNullOrWhiteSpace(driverLogFilePath) Then driverLogFilePath = Path.Combine(userDataDir, $"{profileName}\chromedriver.log") End If Dim options As New ChromeOptions() With { .AcceptInsecureCertificates = True, .EnableDownloads = False, .LeaveBrowserRunning = False, .UnhandledPromptBehavior = UnhandledPromptBehavior.Ignore, .UseStrictFileInteractability = True, .BinaryLocation = chromeFilePath } options.AddAdditionalOption("useAutomationExtension", False) options.AddExcludedArgument("enable-automation") ' Add a set of default arguments optimized for browser automation. Dim currentArgs As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) From { "--allow-insecure-localhost", "--disable-background-networking", "--disable-backgrounding-occluded-windows", "--disable-blink-features=AutomationControlled", "--disable-default-apps", "--disable-dev-shm-usage", "--disable-features=Translate,TranslateUI", "--disable-hang-monitor", "--disable-notifications", "--disable-popup-blocking", "--disable-prompt-on-repost", "--disable-sync", "--ignore-certificate-errors", "--ignore-ssl-errors", "--lang=en-US", "--no-first-run", "--no-sandbox", "--noerrdialogs", "--remote-debugging-port=0", ' Or "--remote-debugging-pipe" "--test-type", $"--user-data-dir={userDataDir}", ' Do NOT use double quotes around the path, as Chrome does not recognize it. $"--profile-directory={profileName}" ' Do NOT use double quotes around the name, as Chrome does not recognize it. } ' Add additional specific optimized arguments if headless mode is specified by the caller. If headless Then Dim headlessAdditionalArgs As String() = { "--headless=New", "--start-maximized", "--disable-site-isolation-trials", "--disable-web-security", "--disable-gpu" } currentArgs.AddRange(headlessAdditionalArgs) End If ' Add additional arguments specified by the caller, avoiding duplicates with the current arguments. If additionalArguments IsNot Nothing AndAlso additionalArguments.Length > 0 Then Dim filteredAdditionalArgs As IEnumerable(Of String) = From arg As String In additionalArguments Where Not String.IsNullOrWhiteSpace(arg) Select arg.Trim() currentArgs.AddRange(additionalArguments) End If ' Add the final set of arguments to ChromeOptions. options.AddArguments(currentArgs) ' Initialize ChromeDriverService with the specified driver file path and log file path, and configure its options. refDriverService = ChromeDriverService.CreateDefaultService(driverDirPath, driveFileName) With refDriverService .DisableBuildCheck = False .EnableAppendLog = False .EnableVerboseLogging = False .HideCommandPromptWindow = True .LogLevel = Chromium.ChromiumDriverLogLevel.Info .LogPath = driverLogFilePath .ReadableTimestamp = True .SuppressInitialDiagnosticInformation = False ' Note: If True, it hangs ChromeDriver initialization. End With ' chromedriver.exe sometimes may cause an error if log file does not exists. If Not File. Exists(driverLogFilePath ) Then Try Directory.CreateDirectory(Path.GetDirectoryName(driverLogFilePath)) Catch ' Ignore errors. End Try Try File. WriteAllText(driverLogFilePath, String. Empty) Catch ' Ignore errors. End Try End If Dim driver As New ChromeDriver(refDriverService, options) ' Hide the Chrome.exe window / taskbar icon if not running in headless mode and specified by the caller. If Not headless AndAlso hideNonHeadlessWindow Then Dim chromeChilds As List(Of Process) = GetChildProcesses(refDriverService.ProcessId) For Each chromeChild As Process In chromeChilds Dim hwnd As IntPtr = chromeChild.MainWindowHandle If hwnd <> IntPtr.Zero Then Const SW_HIDE As Integer = 0 NativeMethods.ShowWindow(hwnd, SW_HIDE) End If Next End If Return driver End Function
Módulo necesario NativeMethods: Public Module NativeMethods <DllImport("user32.dll", SetLastError:=True)> Public Function ShowWindow(hWnd As IntPtr, nCmdShow As Integer) As Boolean End Function End Module
Aquí les dejo también una función llamada KillDriverAndChildBrowsers, cuya finalidad es realizar una limpieza forzada de procesos asociados a drivers de automatización (como chromedriver) que hayan sido lanzados por la aplicación. Esta función resulta especialmente útil en escenarios donde las sesiones de automatización no se cierran correctamente, ya sea por errores inesperados o cierres abruptos del proceso principal. En estos casos, quedarán procesos huérfanos corriendo en segundo plano, tanto del driver como de los navegadores asociados. Esta función, la cual se debería llamar al conrolar el cierre de nuestro proceso principal, actúa como una medida de seguridad para garantizar una limpieza completa del entorno de ejecución del driver y el navegador, evitando acumulación de procesos residuales. ''' <summary> ''' Forcefully terminates all instances of a specific Selenium driver process (e.g., "chromedriver") ''' that were spawned as child processes of the current application. ''' </summary> ''' ''' <remarks> ''' This method is intended as a cleanup safeguard to prevent orphaned driver and child browser processes ''' when the application shuts down unexpectedly or fails to properly dispose Selenium sessions. ''' <para></para> ''' It ensures that any child processes created during automated browser sessions are fully terminated, ''' avoiding background instances that may continue running after the current process exits. ''' </remarks> ''' ''' <param name="driverName"> ''' The name of the driver executable to terminate, without the extension. ''' For example, <c>"chromedriver"</c>. ''' </param> <DebuggerStepThrough> Public Shared Sub KillDriverAndChildBrowsers(driverName As String) #If NETCOREAPP Then ArgumentNullException.ThrowIfNullOrWhiteSpace(driverName, NameOf(driverName)) Dim processId As Integer = Environment.ProcessId #Else If String.IsNullOrWhiteSpace(driverName) Then Throw New ArgumentNullException(NameOf(driverName)) End If Dim processId As Integer = Process.GetCurrentProcess().Id #End If Dim childProcesses As List(Of Process) = GetChildProcesses(processId) Dim childDriverProcesses As IEnumerable(Of Process) = From p As Process In childProcesses Where Not p?.HasExited AndAlso p?.ProcessName.Equals(driverName, StringComparison.InvariantCultureIgnoreCase) For Each p As Process In childDriverProcesses #If NETCOREAPP Then p.Kill(entireProcessTree:=True) #Else Using taskkillProcess As New Process() taskkillProcess.StartInfo.FileName = "taskkill" taskkillProcess.StartInfo.Arguments = $"/PID {p.Id} /T /F" taskkillProcess.StartInfo.UseShellExecute = False taskkillProcess.StartInfo.CreateNoWindow = True taskkillProcess.Start() End Using #End If Next End Sub
Por último, comparto esta otra función utilitaria, GetChildProcesses, que es necesaria por las otras dos funciones que he compartido en este mismo post. ''' <summary> ''' Retrieves a list of child processes for the specified parent process ID. ''' </summary> ''' ''' <param name="parentProcessId"> ''' The ID of the parent process whose child processes should be retrieved. ''' </param> ''' ''' <returns> ''' A <see cref="List(Of Process)"/> containing all child processes of the specified parent process. ''' </returns> <DebuggerStepThrough> Private Shared Function GetChildProcesses(parentProcessId As Integer) As List(Of Process) Dim children As New List(Of Process)() Dim scope As New ManagementScope("root\CIMV2") Dim query As New ObjectQuery($"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId={parentProcessId}") Dim options As New System.Management.EnumerationOptions() With { .EnsureLocatable = False, .ReturnImmediately = True, .Rewindable = False, .Timeout = TimeSpan.FromSeconds(5) } scope.Connect() Using searcher As New ManagementObjectSearcher(scope, query, options) For Each proc As ManagementObject In searcher.Get() Dim pid As Integer = Convert.ToInt32(proc("ProcessId")) Try Dim childProc As Process = Process.GetProcessById(pid) children.Add(childProc) Catch ' Ignore. Process can no longer exists. End Try Next End Using Return children End Function
|
|
|
|
|
10
|
Programación / .NET (C#, VB.NET, ASP) / Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
|
en: 14 Mayo 2026, 16:25 pm
|
Hoy les traigo varios herramientas de utilidad general para la automatización web mediante Selenium. Primero les comparto estas dos funciones, una síncrona y la otra asíncrona, que sirven para inicializar automáticamente el entorno de Selenium utilizando Selenium Manager, descargando y resolviendo dinámicamente tanto el navegador Chrome como ChromeDriver, sin necesidad de mantener versiones sincronizadas entre estos. Uno de los problemas clásicos al trabajar con Selenium es la gestión de versiones entre Chrome y ChromeDriver. Cuando cualquiera de estos componentes queda desincronizado, aparecen errores típicos como: This version of ChromeDriver only supports Chrome version XXX Estas dos funciones que comparto, eliminan completamente ese problema mediante el uso de Selenium Manager para la descarga automática de binarios y la resolución dinámica de rutas, y pudiendo especificar una ruta de caché local configurable. Estas funciones deben utilizarse siempre ANTES de cualquier uso de Selenium dentro del proceso actual. Simplemente llamar a una de estas dos funciones, y olvidarse de problemas. Ejemplo de uso: Dim seleniumCacheDirPath As String = Path.Combine(My.Application.Info.DirectoryPath, "cache\selenium") Dim forceBrowserDownload As Boolean = True Dim result As SeleniumEnvironmentInitializationResult = InitializeSeleniumEnvironmentForChrome(seleniumCacheDirPath, forceBrowserDownload) Console.WriteLine($"Selenium Manager exit code: {result.SeleniumManagerExitCode}") Console.WriteLine($"Driver located at: {result.DriverFilePath}") Console.WriteLine($"Browser binary located at: {result.BrowserFilePath}")
Paquetes NuGet necesarios: - https://www.nuget.org/packages/selenium.webdriver - https://www.nuget.org/packages/selenium.webdriver.chromedriverEl código: ''' <summary> ''' Initializes Selenium for the current process by configuring a ''' local cache directory path through the <c>"SE_CACHE_PATH"</c> environment variable, ''' and optionally forcing the download of latest Chrome and ChromeDriver executables into the specified cache directory. ''' <para></para> ''' This method must be called before any Selenium usage in the current process ''' to ensure that Selenium Manager can locate the ChromeDriver and Chrome browser executables. ''' </summary> ''' ''' <example> ''' This example shows how to initialize the environment: ''' <code language="VB"> ''' Dim cacheDirPath As String = Path.Combine(My.Application.Info.DirectoryPath, "Cache\Selenium") ''' Dim forceBrowserDownload As Boolean = True ''' Dim result As SeleniumEnvironmentInitializationResult = ''' InitializeSeleniumEnvironmentForChrome(cacheDirPath, forceBrowserDownload) ''' ''' Console.WriteLine($"Selenium Manager exit code: {result.SeleniumManagerExitCode}") ''' Console.WriteLine($"Driver located at: {result.DriverFilePath}") ''' Console.WriteLine($"Browser binary located at: {result.BrowserFilePath}") ''' </code> ''' </example> ''' ''' <param name="cacheDirPath"> ''' The directory path where Chrome and ChromeDriver will be stored. For example, <c>".\cache\Selenium"</c> ''' </param> ''' ''' <param name="forceBrowserDownload"> ''' A <see cref="Boolean"/> value indicating whether to force the download of latest Chrome ''' binaries in the directory specified in <paramref name="cacheDirPath"/> parameter. ''' </param> ''' ''' <param name="seleniumManagerFilePath"> ''' Optional. Full path to <c>selenium-manager.exe</c> file. ''' <para></para> ''' If not specified, the default runtime path inside the current application directory is used: ''' <c>".\runtimes\win\native\selenium-manager.exe"</c> ''' </param> ''' ''' <returns> ''' A <see cref="SeleniumEnvironmentInitializationResult"/> object containing the Selenium Manager process exit code, ''' resolved Selenium driver file path, and resolved browser binary file path. ''' </returns> ''' ''' <exception cref="FileNotFoundException"> ''' Thrown when the Selenium Manager file path cannot be resolved, ''' or if the Selenium Manager process execution resolves driver or browser file paths that do not exist. ''' </exception> ''' ''' <exception cref="TimeoutException"> ''' Thrown when the execution of Selenium Manager process exceeds the allowed time limit (10 minutes). ''' </exception> ''' ''' <exception cref="InvalidOperationException"> ''' Thrown when Selenium Manager execution completes successfully but the expected output information ''' (driver and browser paths) cannot be determined or validated. ''' </exception> <DebuggerStepThrough> Public Shared Function InitializeSeleniumEnvironmentForChrome(cacheDirPath As String, forceBrowserDownload As Boolean, Optional seleniumManagerFilePath As String = Nothing) As SeleniumEnvironmentInitializationResult If String.IsNullOrEmpty(seleniumManagerFilePath) Then #If NETCOREAPP Then seleniumManagerFilePath = Path.Combine(AppContext.BaseDirectory, "runtimes\win\native\selenium-manager.exe") #Else seleniumManagerFilePath = Path.Combine(My.Application.Info.DirectoryPath, "runtimes\win\native\selenium-manager.exe") #End If If Not File. Exists(seleniumManagerFilePath ) Then Throw New FileNotFoundException("selenium-manager.exe not found.", seleniumManagerFilePath) End If End If ' Set env var for this process (it MUST be done before any Selenium usage). Environment.SetEnvironmentVariable("SE_CACHE_PATH", cacheDirPath, EnvironmentVariableTarget.Process) Dim argumentsList As New List(Of String) From { If(forceBrowserDownload, "--force-browser-download", String.Empty), "--browser chrome", "--driver chromedriver", $"--cache-path ""{cacheDirPath}""" } Dim arguments As String = argumentsList.Where(Function(arg) Not String.IsNullOrWhiteSpace(arg)).Aggregate(Function(acc, arg) $"{acc} {arg}") Dim outputBuilder As New StringBuilder() Dim errorBuilder As New StringBuilder() Using p As New Process With p.StartInfo .FileName = seleniumManagerFilePath .Arguments = arguments .UseShellExecute = False .RedirectStandardOutput = True .RedirectStandardError = True .CreateNoWindow = True .StandardOutputEncoding = Encoding.UTF8 .StandardErrorEncoding = Encoding.UTF8 .WindowStyle = ProcessWindowStyle.Hidden End With AddHandler p.OutputDataReceived, Sub(sender As Object, e As DataReceivedEventArgs) If Not String.IsNullOrWhiteSpace(e.Data) Then outputBuilder.AppendLine(e.Data) End If End Sub AddHandler p.ErrorDataReceived, Sub(sender As Object, e As DataReceivedEventArgs) If Not String.IsNullOrWhiteSpace(e.Data) Then errorBuilder.AppendLine(e.Data) End If End Sub p.Start() p.BeginOutputReadLine() p.BeginErrorReadLine() Dim exited As Boolean = p.WaitForExit(CInt(TimeSpan.FromMinutes(10).TotalMilliseconds)) If Not exited Then Try p.Kill() Catch End Try Throw New TimeoutException("selenium-manager.exe execution timed out.") End If ' Ensure async buffers are fully flushed. p.WaitForExit() Dim combinedOutput As String = $"{outputBuilder}{Environment.NewLine}{errorBuilder}" ' Example: ' [INFO] Driver path: C:\...\chromedriver.exe ' [INFO] Browser path: C:\...\chrome.exe Dim driverMatch As Match = Regex.Match(combinedOutput, "Driver path:\s*(.+?chromedriver\.exe)", RegexOptions.IgnoreCase Or RegexOptions.Multiline) Dim browserMatch As Match = Regex.Match(combinedOutput, "Browser path:\s*(.+?chrome\.exe)", RegexOptions.IgnoreCase Or RegexOptions.Multiline) Dim result As New SeleniumEnvironmentInitializationResult( p.ExitCode, combinedOutput, If(driverMatch.Success, driverMatch.Groups(1).Value.Trim(), String.Empty), If(browserMatch.Success, browserMatch.Groups(1).Value.Trim(), String.Empty) ) ' Extra validation for successful execution. If p.ExitCode = 0 Then If String.IsNullOrWhiteSpace(result.DriverFilePath) Then Throw New InvalidOperationException( $"Could not determine ChromeDriver path from Selenium Manager output.{Environment.NewLine}{Environment.NewLine}" & $"Output:{Environment.NewLine}{combinedOutput}") End If If String.IsNullOrWhiteSpace(result.BrowserFilePath) Then Throw New InvalidOperationException( $"Could not determine Chrome browser path from Selenium Manager output.{Environment.NewLine}{Environment.NewLine}" & $"Output:{Environment.NewLine}{combinedOutput}") End If If Not File. Exists(result. DriverFilePath) Then Throw New FileNotFoundException( "Resolved ChromeDriver.exe file was not found.", result.DriverFilePath) End If If Not File. Exists(result. BrowserFilePath) Then Throw New FileNotFoundException( "Resolved Chrome.exe file was not found.", result.BrowserFilePath) End If End If Return result End Using End Function ''' <summary> ''' Asynchronously initializes Selenium for the current process by configuring a ''' local cache directory path through the <c>"SE_CACHE_PATH"</c> environment variable, ''' and optionally forcing the download of latest Chrome and ChromeDriver executables into the specified cache directory. ''' <para></para> ''' This method must be called before any Selenium usage in the current process ''' to ensure that Selenium Manager can locate the ChromeDriver and Chrome browser executables. ''' </summary> ''' ''' <example> ''' This example shows how to initialize the environment: ''' <code language="VB"> ''' Dim cacheDirPath As String = Path.Combine(My.Application.Info.DirectoryPath, "Cache\Selenium") ''' Dim forceBrowserDownload As Boolean = True ''' Dim result As SeleniumEnvironmentInitializationResult = ''' Await InitializeSeleniumEnvironmentForChromeAsync(cacheDirPath, forceBrowserDownload) ''' ''' Console.WriteLine($"Selenium Manager exit code: {result.SeleniumManagerExitCode}") ''' Console.WriteLine($"Driver located at: {result.DriverFilePath}") ''' Console.WriteLine($"Browser binary located at: {result.BrowserFilePath}") ''' </code> ''' </example> ''' ''' <param name="cacheDirPath"> ''' The directory path where Chrome and ChromeDriver will be stored. For example, <c>".\cache\Selenium"</c> ''' </param> ''' ''' <param name="forceBrowserDownload"> ''' A <see cref="Boolean"/> value indicating whether to force the download of latest Chrome ''' binaries in the directory specified in <paramref name="cacheDirPath"/> parameter. ''' </param> ''' ''' <param name="seleniumManagerFilePath"> ''' Optional. Full path to <c>selenium-manager.exe</c> file. ''' <para></para> ''' If not specified, the default runtime path inside the current application directory is used: ''' <c>".\runtimes\win\native\selenium-manager.exe"</c> ''' </param> ''' ''' <returns> ''' A <see cref="Task(Of SeleniumEnvironmentInitializationResult)"/> representing the asynchronous operation, ''' containing the Selenium Manager process exit code, resolved Selenium driver file path, and resolved browser binary file path. ''' </returns> ''' ''' <exception cref="FileNotFoundException"> ''' Thrown when the Selenium Manager file path cannot be resolved, ''' or if the Selenium Manager process execution resolves driver or browser file paths that do not exist. ''' </exception> ''' ''' <exception cref="TimeoutException"> ''' Thrown when the execution of Selenium Manager process exceeds the allowed time limit (10 minutes). ''' </exception> ''' ''' <exception cref="InvalidOperationException"> ''' Thrown when Selenium Manager execution completes successfully but the expected output information ''' (driver and browser paths) cannot be determined or validated. ''' </exception> <DebuggerStepThrough> Public Shared Async Function InitializeSeleniumEnvironmentForChromeAsync(cacheDirPath As String, forceBrowserDownload As Boolean, Optional seleniumManagerFilePath As String = Nothing) As Task(Of SeleniumEnvironmentInitializationResult) Return Await Task.Run( Function() As SeleniumEnvironmentInitializationResult Return InitializeSeleniumEnvironmentForChrome(cacheDirPath, forceBrowserDownload, seleniumManagerFilePath) End Function ).ConfigureAwait(continueOnCapturedContext:=False) End Function
Clase necesria SeleniumEnvironmentInitializationResult, que representa el resultado de la inicialización: #Region " Option Statements " Option Strict On Option Explicit On Option Infer Off #End Region #Region " Imports " #End Region #Region " SeleniumEnvironmentInitializationResult " 'Namespace DevCase.ThirdParty.Selenium ''' <summary> ''' Represents the result of Selenium environment initialization process performed by ''' functions like <see cref="UtilSelenium.InitializeSeleniumEnvironmentForChrome"/> ''' and <see cref="UtilSelenium.InitializeSeleniumEnvironmentForChromeAsync"/>, ''' containing the Selenium Manager process exit code, resolved Selenium driver file path, ''' and resolved browser binary file path. ''' </summary> Public NotInheritable Class SeleniumEnvironmentInitializationResult #Region " Properties " ''' <summary> ''' Gets the exit code returned by the Selenium Manager process execution. ''' <para></para> ''' A value of 0 indicates success, while non-zero values indicate an error condition. ''' </summary> Public ReadOnly Property SeleniumManagerExitCode As Integer ''' <summary> ''' Gets the console output produced by the Selenium Manager process execution, ''' which may contain informational messages, warnings, or error details related to the environment initialization process. ''' </summary> Public ReadOnly Property SeleniumManagerConsoleOutput As String ''' <summary> ''' Gets the full file path of the resolved Selenium driver executable, ''' as determined by Selenium Manager process execution. ''' </summary> Public ReadOnly Property DriverFilePath As String ''' <summary> ''' Gets the full file path of the resolved browser executable, ''' as determined by Selenium Manager process execution. ''' </summary> Public ReadOnly Property BrowserFilePath As String #End Region #Region " Constructors " ''' <summary> ''' Prevents a default instance of the <see cref="SeleniumEnvironmentInitializationResult"/> class from being created. ''' </summary> Private Sub New() End Sub ''' <summary> ''' Initializes a new instance of the <see cref="SeleniumEnvironmentInitializationResult"/> class. ''' </summary> ''' ''' <param name="seleniumManagerExitCode"> ''' The Selenium Manager exit code. ''' </param> ''' ''' <param name="seleniumManagerConsoleOutput"> ''' The Selenium Manager console output, ''' which may contain informational messages, warnings, ''' or error details related to the environment initialization process. ''' </param> ''' ''' <param name="driverFilePath"> ''' The resolved Selenium driver file path. ''' </param> ''' ''' <param name="browserFilePath"> ''' The resolved browser file path. ''' </param> Public Sub New(seleniumManagerExitCode As Integer, seleniumManagerConsoleOutput As String, driverFilePath As String, browserFilePath As String) Me.SeleniumManagerExitCode = seleniumManagerExitCode Me.seleniumManagerConsoleOutput = seleniumManagerConsoleOutput Me.DriverFilePath = driverFilePath Me.BrowserFilePath = browserFilePath End Sub #End Region End Class 'End Namespace #End Region
Adicionalmente, les dejo por aquí estas otras dos funciones, que sirven para resolver de forma programática la ruta a la versión de chrome y chromedriver más reciente dentro del directorio de cache local de Selenium: GetLatestCachedChromeDriverFilePath ''' <summary> ''' Resolves the full file path of the most recent (latest) cached Chrome browser binary version ''' available in the specified Selenium cache directory path. ''' </summary> ''' ''' <example> This is a code example. ''' <code language="VB"> ''' Dim seleniumCacheDirPath As String = Nothing ' Or set to a specific path if desired. For example, ".\Cache\Selenium". ''' Dim latestChromeDriverFilePath As String = GetLatestCachedChromeDriverFilePath(seleniumCacheDirPath) ''' Console.WriteLine($"Latest ChromeDriver.exe File Path: {latestChromeDriverFilePath}") ''' </code> ''' </example> ''' ''' <param name="seleniumCacheDirPath"> ''' Optional. The Selenium cache directory path. For example, ".\Cache\Selenium". ''' <para></para> ''' This is the base directory where Selenium Manager stores downloaded browser binaries and drivers. ''' <para></para> ''' If this value is not set, the function will attempt to read the cache path from ''' the <c>SE_CACHE_PATH</c> environment variable for the current process. ''' </param> ''' ''' <returns> ''' A <see cref="String"/> containing the full file path to the latest Chrome version directory. ''' </returns> ''' ''' <exception cref="DirectoryNotFoundException"> ''' Thrown when the Selenium cache directory path does not exist, ''' or when the base directory for Chrome versioned directories is not found. ''' </exception> ''' ''' <exception cref="InvalidOperationException"> ''' Thrown when the Selenium cache directory path is not provided and <c>SE_CACHE_PATH</c> environment variable is not set, ''' or when no valid versioned Chrome directories are found in the expected location. ''' </exception> <DebuggerStepThrough> Public Shared Function GetLatestCachedChromeDriverFilePath(Optional seleniumCacheDirPath As String = Nothing) As String If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then seleniumCacheDirPath = Environment.GetEnvironmentVariable("SE_CACHE_PATH", EnvironmentVariableTarget.Process) If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then Throw New InvalidOperationException("Selenium cache directory path is not provided and SE_CACHE_PATH environment variable is not set.") End If If Not Directory.Exists(seleniumCacheDirPath) Then Throw New DirectoryNotFoundException($"Selenium cache directory specified in SE_CACHE_PATH does not exist: {seleniumCacheDirPath}") End If ElseIf Not String.IsNullOrWhiteSpace(seleniumCacheDirPath) AndAlso Not Directory.Exists(seleniumCacheDirPath) Then Throw New DirectoryNotFoundException($"The provided Selenium cache directory path does not exist: {seleniumCacheDirPath}") End If Dim baseDirPath As String = Path.Combine(seleniumCacheDirPath, "chromedriver", "win64") If Not Directory.Exists(baseDirPath) Then Throw New DirectoryNotFoundException($"Base directory for ChromeDrover versioned directories does not exist: {baseDirPath}") End If Dim subDirs As String() = Directory.GetDirectories(baseDirPath, "*.*", SearchOption.TopDirectoryOnly) Dim latestVersionedDir As String = subDirs.Select(Function(dir As String) New With {.Path = dir, .FolderName = Path.GetFileName(dir)}). Where(Function(x) Dim ver As Version = Nothing Return Version.TryParse(x.FolderName, ver) End Function). OrderByDescending(Function(x) New Version(x.FolderName)). Select(Function(x) x.Path). FirstOrDefault() If String.IsNullOrEmpty(latestVersionedDir) Then Throw New InvalidOperationException($"No valid versioned ChromeDriver directories were found in: ""{baseDirPath}""") End If Return Path.Combine(latestVersionedDir, "chromedriver.exe") End Function
GetLatestCachedChromeFilePath ''' <summary> ''' Resolves the full file path of the most recent (latest) cached Chrome browser binary version ''' available in the specified Selenium cache directory path. ''' </summary> ''' ''' <example> This is a code example. ''' <code language="VB"> ''' Dim seleniumCacheDirPath As String = Nothing ' Or set to a specific path if desired. For example, ".\Cache\Selenium". ''' Dim latestChromeFilePath As String = GetLatestCachedChromeFilePath(seleniumCacheDirPath) ''' Console.WriteLine($"Latest Chrome.exe File Path: {latestChromeFilePath}") ''' </code> ''' </example> ''' ''' <param name="seleniumCacheDirPath"> ''' Optional. The Selenium cache directory path. For example, ".\Cache\Selenium". ''' <para></para> ''' This is the base directory where Selenium Manager stores downloaded browser binaries and drivers. ''' <para></para> ''' If this value is not set, the function will attempt to read the cache path from ''' the <c>SE_CACHE_PATH</c> environment variable for the current process. ''' </param> ''' ''' <returns> ''' A <see cref="String"/> containing the full file path to the latest Chrome version directory. ''' </returns> ''' ''' <exception cref="DirectoryNotFoundException"> ''' Thrown when the Selenium cache directory path does not exist, ''' or when the base directory for Chrome versioned directories is not found. ''' </exception> ''' ''' <exception cref="InvalidOperationException"> ''' Thrown when the Selenium cache directory path is not provided and <c>SE_CACHE_PATH</c> environment variable is not set, ''' or when no valid versioned Chrome directories are found in the expected location. ''' </exception> <DebuggerStepThrough> Public Shared Function GetLatestCachedChromeFilePath(Optional seleniumCacheDirPath As String = Nothing) As String If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then seleniumCacheDirPath = Environment.GetEnvironmentVariable("SE_CACHE_PATH", EnvironmentVariableTarget.Process) If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then Throw New InvalidOperationException("Selenium cache directory path is not provided and SE_CACHE_PATH environment variable is not set.") End If If Not Directory.Exists(seleniumCacheDirPath) Then Throw New DirectoryNotFoundException($"Selenium cache directory specified in SE_CACHE_PATH does not exist: {seleniumCacheDirPath}") End If ElseIf Not String.IsNullOrWhiteSpace(seleniumCacheDirPath) AndAlso Not Directory.Exists(seleniumCacheDirPath) Then Throw New DirectoryNotFoundException($"The provided Selenium cache directory path does not exist: {seleniumCacheDirPath}") End If Dim baseDirPath As String = Path.Combine(seleniumCacheDirPath, "chrome", "win64") If Not Directory.Exists(baseDirPath) Then Throw New DirectoryNotFoundException($"Base directory for Chrome versioned directories does not exist: {baseDirPath}") End If Dim subDirs As String() = Directory.GetDirectories(baseDirPath, "*.*", SearchOption.TopDirectoryOnly) Dim latestVersionedDir As String = subDirs.Select(Function(dir As String) New With {.Path = dir, .FolderName = Path.GetFileName(dir)}). Where(Function(x) Dim ver As Version = Nothing Return Version.TryParse(x.FolderName, ver) End Function). OrderByDescending(Function(x) New Version(x.FolderName)). Select(Function(x) x.Path). FirstOrDefault() If String.IsNullOrEmpty(latestVersionedDir) Then Throw New InvalidOperationException($"No valid versioned Chrome directories were found in: ""{baseDirPath}""") End If Return Path.Combine(latestVersionedDir, "chrome.exe") End Function
|
|
|
|
|
|
| |
|