So lösen Sie Captchas in einem .NET Worker Service, der zuverlässig weiterläuft

worker service captcha - How to Solve CAPTCHAs in a .NET Worker Service That Stays Up

Um Captchas in einem .NET Worker Service zu lösen, registrieren Sie einen einzigen CapSkipClient als Singleton, rufen RecaptchaAsync mit dem Stopping-Token aus ExecuteAsync auf und fangen die Exceptions von CapSkip innerhalb der Schleife ab. Genau diesen letzten Teil lässt die Worker-Vorlage weg, und er ist wichtiger, als er aussieht. Seit .NET 6 stoppt eine unbehandelte Exception in einem BackgroundService den gesamten Host. Der Prozess beendet sich dann mit Exit-Code 0, sodass die Wiederherstellungsoptionen eines Windows Service ihn nie neu starten. Eine einzige NetworkException beim Booten, ausgelöst, weil CapSkip noch nicht geöffnet war, reicht dafür schon aus. Dieses Captcha-Setup für den Worker Service meldet sich alle 15 Minuten bei einer Website hinter reCAPTCHA an, wartet einen Solver ab, der noch nicht bereit ist, lässt sich in Sekundenbruchteilen stoppen und läuft als Windows Service neben CapSkip.

Was Sie brauchen

  • CapSkip auf einem Windows-Rechner, mit eingeschaltetem reCAPTCHA-Löser.
  • Das .NET 8 SDK oder neuer. Der Code unten wurde unter .NET 8 und .NET 10 ausgeführt.
  • Das CapSkip-Paket von NuGet, dazu Microsoft.Extensions.Hosting.WindowsServices, das Program.cs unten nutzt, um als Windows Service zu laufen.
  • Administratorrechte auf dem Rechner, auf dem Sie den Dienst installieren, und nur für diesen Schritt.
  • Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Worker auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi unter Verbindungseinstellungen.
dotnet new worker -n PortalWorker
cd PortalWorker
dotnet add package CapSkip
# Update Hosting before the next line: the template pins an older version,
# and the Windows Services package fails to restore against it (NU1605).
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Die Reihenfolge der letzten beiden Zeilen ist wichtig. Die Worker-Vorlage legt die Version von Microsoft.Extensions.Hosting fest, die mit Ihrem SDK ausgeliefert wurde, und das aktuelle Paket für Windows Services braucht eine neuere. Fügen Sie das Paket für Windows Services zuerst hinzu, scheitert der Restore mit NU1605, einem Fehler wegen eines Paket-Downgrades. Wenn Sie zuerst das Hosting-Paket aktualisieren, vermeiden Sie das, und sonst ändert sich nichts an der Vorlage.

Schritt 1: Einen einzigen Client in Program.cs registrieren

CapSkipClient hält keinen Zustand pro Anfrage und sendet alles über einen einzigen statischen HttpClient innerhalb des Pakets, daher kann eine einzige Instanz den ganzen Prozess bedienen. Erzeugen Sie ihn einmal aus der Konfiguration und registrieren Sie ihn als Singleton.

// Program.cs
using CapSkip;
using PortalWorker;

var builder = Host.CreateApplicationBuilder(args);

// A Windows Service when the service manager starts it, an ordinary
// console app when you run it yourself.
builder.Services.AddWindowsService(options => options.ServiceName = "PortalWorker");

// One client for the whole process. The SDK reads no environment
// variables, but the host's configuration does, so CAPSKIP_HOST can come
// from the environment or from appsettings.json.
var config = builder.Configuration;
builder.Services.AddSingleton(new CapSkipClient(
    apiKey: config["CAPSKIP_API_KEY"] ?? "capskip",
    host: config["CAPSKIP_HOST"] ?? "127.0.0.1",
    port: int.Parse(config["CAPSKIP_PORT"] ?? "8080")));
builder.Services.AddSingleton<PortalLogin>();
builder.Services.AddHostedService<Worker>();

builder.Build().Run();

AddWindowsService tut nichts, wenn Sie das Programm selbst starten, daher läuft derselbe Build beim Testen in einer Konsole und später als Dienst. Das CapSkip SDK liest von sich aus keine Umgebungsvariablen, aber das Konfigurationsobjekt hinter Host.CreateApplicationBuilder liest jede Umgebungsvariable, daher funktioniert CAPSKIP_HOST aus der Umgebung. Es funktioniert auch als Schlüssel in appsettings.json, und das ist für einen Dienst der einfachere Ort, denn die Umgebung eines Dienstes lässt sich nur umständlich bearbeiten. Wenn Windows einen Dienst startet, ist sein Arbeitsordner System32. Der Host verwendet dann den eigenen Ordner des Programms als Content Root und liest daher die appsettings.json, die neben der exe veröffentlicht wurde.

Schritt 2: Der Job, der das Captcha löst

Der Job selbst ist gewöhnlicher Code. Er ruft die Login-Seite ab, liest den sitekey und die versteckten Felder aus, fordert bei CapSkip einen Token an und sendet das Formular ab. Legen Sie ihn in eine eigene Klasse, damit der Worker nur entscheidet, wann er läuft und was ein Fehlschlag bedeutet.

// PortalLogin.cs
using System.Net;
using System.Text.RegularExpressions;
using CapSkip;

namespace PortalWorker;

public sealed class PortalLogin(CapSkipClient solver, ILogger<PortalLogin> logger)
{
    const string PageUrl = "https://example.com/login";

    public async Task RunAsync(CancellationToken ct)
    {
        // A new HttpClient per run keeps its own cookies, so one run's
        // session never leaks into the next.
        using var http = new HttpClient();
        var html = await http.GetStringAsync(PageUrl, ct);
        var sitekey = Regex.Match(html, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;

        // The form's hidden fields, such as a CSRF token.
        var fields = new Dictionary<string, string>();
        foreach (Match input in Regex.Matches(html, "<input[^>]*type=\"hidden\"[^>]*>"))
        {
            var name = Regex.Match(input.Value, "name=\"([^\"]+)\"").Groups[1].Value;
            var value = Regex.Match(input.Value, "value=\"([^\"]*)\"").Groups[1].Value;
            fields[name] = WebUtility.HtmlDecode(value);
        }

        // Pass the token on, so stopping the service cancels the solve.
        var result = await solver.RecaptchaAsync(sitekey, PageUrl, cancellationToken: ct);

        fields["username"] = "YOUR_USERNAME";
        fields["password"] = "YOUR_PASSWORD";
        fields["g-recaptcha-response"] = result.Code;
        using var post = new HttpRequestMessage(HttpMethod.Post, PageUrl)
        {
            Content = new FormUrlEncodedContent(fields),
        };
        // HttpClient sends no Referer; strict servers refuse a post without one.
        post.Headers.Referrer = new Uri(PageUrl);
        using var response = await http.SendAsync(post, ct);
        if (response.IsSuccessStatusCode)
            logger.LogInformation("Login posted: {Status}", (int)response.StatusCode);
        else
            logger.LogWarning("Login rejected: {Status}", (int)response.StatusCode);
    }
}

Drei Details übersieht man leicht. Ein neuer HttpClient pro Durchlauf behält Cookies nur für diesen Durchlauf, sodass Session-Cookie und CSRF-Token in den versteckten Feldern noch zusammenpassen, wenn das Formular zurückgeht. HttpClient sendet keinen Referer-Header, und Frameworks wie Django lehnen einen HTTPS-Formular-POST ohne diesen Header ab, daher setzt der Job ihn. Und der Stopping-Token geht an RecaptchaAsync, und genau das erlaubt es dem Dienst, mitten in einem Lösevorgang zu stoppen, statt auf ihn zu warten.

Lösen Sie direkt vor dem Absenden. Ein reCAPTCHA-Token läuft zwei Minuten nach seiner Ausgabe ab, ein Worker, der früh löst und später absendet, schickt also einen toten Token. Das Parsen der Seite ist hier bewusst knapp gehalten. Unsichtbare Widgets, Enterprise-Schlüssel und andere Varianten behandelt der Leitfaden zum Lösen von reCAPTCHA v2 in C#.

Schritt 3: Eine Schleife, die einen gescheiterten Lösevorgang übersteht

Die meisten Captcha-Bugs in Worker Services stecken in dieser Datei. Die Schleife der Vorlage hat überhaupt keinen try-Block, und das ist in Ordnung, um die Uhrzeit zu loggen, aber fatal für alles, was mit einem anderen Programm spricht.

// Worker.cs
using CapSkip;

namespace PortalWorker;

public sealed class Worker(PortalLogin job, ILogger<Worker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var wait = TimeSpan.FromMinutes(15);
                try
                {
                    await job.RunAsync(stoppingToken);
                }
                catch (NetworkException ex) when (!stoppingToken.IsCancellationRequested)
                {
                    // CapSkip is not running yet, as after a reboot. Try again soon.
                    logger.LogWarning("CapSkip unreachable: {Message}", ex.Message);
                    wait = TimeSpan.FromSeconds(30);
                }
                catch (CapSkipError ex) when (!stoppingToken.IsCancellationRequested)
                {
                    logger.LogWarning("Solve failed: {Type}: {Message}", ex.GetType().Name, ex.Message);
                }
                catch (Exception ex) when ((ex is HttpRequestException or TaskCanceledException)
                                           && !stoppingToken.IsCancellationRequested)
                {
                    // The site failed, or HttpClient gave up on it after 100 s.
                    logger.LogWarning("The site did not answer: {Message}", ex.Message);
                }

                await Task.Delay(wait, stoppingToken);
            }
        }
        catch (Exception) when (stoppingToken.IsCancellationRequested)
        {
            // Stopping. A cancelled poll ends in TaskCanceledException and a
            // cancelled submit in NetworkException; neither is a failure.
        }
        catch (Exception ex)
        {
            logger.LogCritical(ex, "Worker failed");
            // Exit non-zero, so the service's recovery options restart it.
            // A host that stops on its own exits with code 0.
            Environment.Exit(1);
        }
    }
}

Jeder catch-Block beantwortet eine Frage. Eine NetworkException bedeutet, dass CapSkip nicht geantwortet hat, meist weil es noch nicht läuft, daher versucht es der Worker nach 30 Sekunden erneut statt nach 15 Minuten. Jeder andere CapSkipError ist ein gescheiterter Lösevorgang, etwa eine ApiException mit einem Fehlercode von CapSkip, daher loggt der Worker ihn und wartet auf den nächsten Durchlauf. Eine HttpRequestException oder eine TaskCanceledException, die eintrifft, während nichts stoppt, kommt von der Website. Eine TaskCanceledException bedeutet hier, dass HttpClient nach seinen standardmäßigen 100 Sekunden aufgegeben hat. Jeder Fehlschlag wird auf Warning geloggt, ein abgelehnter Login eingeschlossen, aus einem Grund, den der Abschnitt zum Dienst erklärt.

Der Filter auf den Stopping-Token verhindert, dass ein Stopp wie ein Fehlschlag aussieht. Der äußere catch-Block behandelt dann den Stopp selbst, und der letzte catch-Block behandelt alles, womit niemand gerechnet hat, etwa einen Bug beim Parsen. Er loggt die Exception und beendet den Prozess mit Exit-Code 1, und genau dieses eine Detail erlaubt es Windows, den Dienst neu zu starten.

Warum eine einzige Exception den ganzen Host stoppt

Der Host startet ExecuteAsync und überwacht den Task, den die Methode zurückgibt. Seit .NET 6 wendet der Host, wenn dieser Task fehlschlägt, HostOptions.BackgroundServiceExceptionBehavior an, dessen Standardwert StopHost ist. Nehmen Sie die Schleife der Vorlage und schließen Sie CapSkip. Ein Durchlauf unter .NET 10 loggt dann BackgroundService failed mit der NetworkException darunter, gefolgt von einer kritischen Logzeile, laut der der Host stoppt, weil ein BackgroundService eine Exception geworfen hat. Dann fährt er herunter. Eine einzige abgelehnte Verbindung, und der Worker ist weg.

Der Exit-Code macht das für einen Dienst so tückisch. Der Host stoppt sauber, also beendet sich der Prozess mit Exit-Code 0, was wir unter .NET 8 und .NET 10 gleichermaßen gemessen haben. Windows wendet die Wiederherstellungsoptionen eines Dienstes nur an, wenn er fehlschlägt, und ein sauberes Beenden ist kein Fehlschlag, also bleibt der Dienst einfach gestoppt. Microsofts eigener Leitfaden zum Ausführen eines BackgroundService als Windows Service empfiehlt aus diesem Grund, Environment.Exit mit einem Code ungleich null aufzurufen, und der Worker oben tut das in seinem letzten catch-Block. Die andere Einstellung, Ignore, ist schlimmer: Der Host läuft mit einem toten Worker darin weiter, ein Dienst, der gesund aussieht und nichts tut.

Mitten in einem Lösevorgang sauber stoppen

Wenn Windows den Dienst stoppt oder Sie in der Konsole Ctrl+C drücken, löst der Host den Stopping-Token aus und wartet, bis ExecuteAsync fertig ist. Er wartet bis zu HostOptions.ShutdownTimeout, standardmäßig 30 Sekunden, und stoppt dann trotzdem. Ein reCAPTCHA-Lösevorgang kann weit länger dauern, denn das SDK pollt bis zu 300 Sekunden. Ohne den Token dauerte in unserem Test ein Stopp, der 5 Sekunden nach Beginn eines 60-sekündigen Lösevorgangs angefordert wurde, 30 Sekunden: Der Host hat ShutdownTimeout voll ausgeschöpft und den Lösevorgang dann aufgegeben. Mit durchgereichtem Token dauerte derselbe Stopp etwa 20 Millisekunden.

Ein Abbruch erreicht Sie in zwei Formen, je nachdem, wo er ankommt. Während des Pollings des SDK endet er in einer TaskCanceledException aus der Verzögerung zwischen zwei Abfragen. Kommt er an, während das SDK die Aufgabe noch übermittelt (seine erste Anfrage an CapSkip), verpackt das SDK ihn in eine NetworkException, und die ist ein CapSkipError und keine OperationCanceledException. Ohne Filter sieht dieser zweite Fall wie ein Absturz aus. Wir haben einen Worker mitten in der Übermittlung gestoppt, und der Host loggte BackgroundService failed mit The operation was canceled darunter, bei einem ganz gewöhnlichen Herunterfahren. Deshalb prüft jeder catch-Block oben zuerst den Stopping-Token.

Schritt 4: Als Windows Service ausführen

Veröffentlichen Sie den Worker in einen Ordner und registrieren Sie die exe beim Dienststeuerungs-Manager. Jeder Befehl unten außer dotnet publish braucht eine PowerShell mit Administratorrechten.

# In an elevated PowerShell. Paths are examples.
dotnet publish -c Release -r win-x64 -o C:\Services\PortalWorker
sc.exe create PortalWorker binpath= "C:\Services\PortalWorker\PortalWorker.exe" start= delayed-auto
sc.exe failure PortalWorker reset= 86400 actions= restart/60000/restart/60000/restart/60000
sc.exe start PortalWorker

Die Zeile mit failure ist das Gegenstück zu Environment.Exit(1). Sie weist Windows an, den Dienst eine Minute nach einem Fehlschlag neu zu starten. Windows wiederholt die letzte Aktion für jeden weiteren Fehlschlag, sodass der Dienst immer wieder zurückkommt, und der Fehlerzähler wird nach einem Tag ohne Fehlschläge zurückgesetzt. Der verzögerte automatische Start lässt den Dienst kurz nach den anderen automatischen Diensten anlaufen statt ganz zu Beginn des Bootvorgangs. CapSkip selbst ist eine Desktop-Anwendung und läuft nach einem Neustart daher erst, wenn sich jemand bei Windows anmeldet, und der Dienst startet unter Umständen lange davor. Der Worker kommt mit diesem Fall von selbst zurecht: Er loggt alle 30 Sekunden CapSkip unreachable, bis der Solver antwortet. Jeder Wiederholungsversuch lädt außerdem die Login-Seite erneut, etwa 120 Mal pro Stunde, solange Windows am Anmeldebildschirm wartet. Verlängern Sie die 30 Sekunden also, falls die Website das bemerken würde. Ein Dienst und eine Desktop-App laufen in verschiedenen Windows-Sitzungen, aber 127.0.0.1 verbindet sitzungsübergreifend, daher funktioniert der Local-Modus zwischen beiden.

Als Dienst sendet der Host seine Logs an das Windows Event Log, genauer an das Application log, und standardmäßig kommen dort nur Einträge ab Level Warning an. Deshalb loggt der Worker seine Fehlschläge als Warnungen: Eine Zeile mit Level Information würde nie im Event Viewer auftauchen.

Mehrere Konten in einem Durchlauf

Um bei jedem Durchlauf mehrere Konten anzumelden, iterieren Sie innerhalb des Jobs mit Parallel.ForEachAsync, statt weitere Worker zu starten. Setzen Sie MaxDegreeOfParallelism auf 10, denn CapSkip löst standardmäßig 10 reCAPTCHAs gleichzeitig (Max. Threads in seinen reCAPTCHA-Einstellungen) und reiht den Rest ein. Übergeben Sie den Stopping-Token im CancellationToken von ParallelOptions, und reichen Sie den Token, den jede Iteration erhält, an RecaptchaAsync weiter, damit ein Stopp alle Lösevorgänge auf einmal abbricht. Fangen Sie die Fehlschläge jedes Kontos innerhalb des Schleifenrumpfs ab, mit denselben Filtern wie im Worker. Eine Exception, die einem Konto entkommt, bricht die Lösevorgänge aller anderen Konten in diesem Durchlauf ab. Das Singleton CapSkipClient lässt sich gefahrlos über diese Aufrufe hinweg teilen. Geben Sie jedem Konto einen eigenen HttpClient, wie es der Job oben tut, damit keine zwei Sessions ein Cookie teilen.

Den Löser auf einer anderen Maschine betreiben

127.0.0.1 ist richtig, solange der Worker und CapSkip auf demselben Windows-PC laufen. Der Worker kann aber auch woanders laufen, auf einem anderen Windows-Server, einer VM oder einem Container-Host. Schalten Sie CapSkip dann in den Server-Modus, damit es auf Ihrer Netzwerkadresse oder öffentlichen IP lauscht, und setzen Sie CAPSKIP_HOST in appsettings.json oder in der Umgebung auf diese Adresse. Verwenden Sie eine statische öffentliche IP, wenn der Weg über das Internet führt, und aktivieren Sie die Validierung des API-Schlüssels. CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse. Um ihn auf die Rechner zu beschränken, die Sie erwarten, fügen Sie in der Windows Firewall eine Blockierregel für alle anderen hinzu. Der Solver bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.

Häufige Captcha-Fehler im Worker Service

Was Sie sehenUrsacheBeheben
NU1605, ein erkanntes Paket-Downgrade, beim Hinzufügen des Pakets für Windows ServicesDie Vorlage legt ein älteres Microsoft.Extensions.Hosting fest, als das Paket für Windows Services brauchtFügen Sie zuerst Microsoft.Extensions.Hosting hinzu, dann das Paket für Windows Services
BackgroundService failed, dann eine kritische Logzeile zu StopHost, und der Worker ist wegEine Exception ist aus ExecuteAsync entkommen; das Standardverhalten StopHost hat den Host gestopptFangen Sie CapSkipError und die Exceptions der Website innerhalb der Schleife ab
Der Dienst hat nach einem Fehler gestoppt, und Windows hat ihn nie neu gestartetEin durch StopHost gestoppter Host beendet sich mit Exit-Code 0, und das ist für Windows kein FehlschlagBeenden Sie unerwartete Fehlschläge mit Environment.Exit(1) und richten Sie die Wiederherstellung mit sc.exe failure ein
BackgroundService failed mit The operation was canceled, bei einem normalen StoppDer Stopp kam an, während die Aufgabe an CapSkip übermittelt wurde, und das SDK verpackt das in eine NetworkExceptionFiltern Sie jeden catch-Block auf den Stopping-Token
Das Stoppen des Dienstes dauert 30 SekundenDer Lösevorgang ignoriert den Abbruch, daher wartet der Host das volle ShutdownTimeout abÜbergeben Sie den Stopping-Token an RecaptchaAsync und an jeden HTTP-Aufruf
CapSkip unreachable im Log nach jedem NeustartDer Dienst ist gestartet, bevor sich jemand angemeldet und CapSkip geöffnet hatKeine Abhilfe nötig: Der Worker versucht es alle 30 Sekunden erneut, bis CapSkip antwortet
NetworkException mit der Meldung, dass der Zielcomputer die Verbindung aktiv verweigert hatCapSkip läuft nicht, oder Host oder Port sind falschStarten Sie CapSkip und gleichen Sie CAPSKIP_HOST und CAPSKIP_PORT mit seinen Verbindungseinstellungen ab
Die Website lehnt den Token ab oder antwortet erneut mit ihrer Login-SeiteDer Token ist vor dem Absenden abgelaufen, oder beim Absenden fehlte das Cookie, ein verstecktes Feld oder der RefererLösen Sie direkt vor dem Absenden, und senden Sie über den HttpClient ab, der die Seite abgerufen hat
Nichts vom Worker im Event ViewerDas Event Log nimmt standardmäßig nur Warning und höher aufLoggen Sie Fehlschläge auf Warning, oder setzen Sie in appsettings.json für EventLog den LogLevel Information
Eine ApiException mit ERROR_KEY_DOES_NOT_EXIST oder ERROR_WRONG_USER_KEYIn CapSkip ist die Validierung des API-Schlüssels aktiv, und der Worker hat den Standardschlüssel oder gar keinen gesendetSetzen Sie CAPSKIP_API_KEY auf einen in CapSkip konfigurierten Schlüssel

FAQ

Sollte ich BackgroundServiceExceptionBehavior stattdessen auf Ignore setzen?

Nein. Mit Ignore loggt der Host die Exception einmal als Fehler und macht weiter, aber ExecuteAsync wird nie wieder aufgerufen, sodass der Prozess weiterläuft, ohne dass darin noch etwas arbeitet. Windows meldet den Dienst als laufend, und es finden keine Logins statt, bis es jemand bemerkt. Fangen Sie die erwarteten Fehlschläge in der Schleife ab und lassen Sie den Rest den Prozess mit einem Exit-Code ungleich null beenden.

Gilt dasselbe für einen Hosted Service in ASP.NET Core?

Ja. Eine App mit ASP.NET Core läuft auf demselben Generic Host. Ein BackgroundService in einer Web-App hat also denselben Standardwert StopHost und dasselbe Shutdown-Timeout von 30 Sekunden, und eine unbehandelte CapSkip-Exception reißt die ganze Website mit. Die Schleife oben funktioniert dort unverändert, aber beachten Sie, dass ihr Environment.Exit(1) auch die ganze Web-App beendet.

Wann passt Hangfire besser als ein Worker?

Wenn die Lösevorgänge Jobs sind, die anderer Code in die Queue stellt, und keine einzelne Aufgabe an einem Timer. Hangfire speichert jeden Job, wiederholt ihn nach eigenem Zeitplan und zeigt den Verlauf in einem Dashboard, was ein einfacher Worker Ihnen überlässt. Sein Retry-Filter braucht im Umgang mit den Exceptions von CapSkip eigene Sorgfalt. Wie das geht, erklärt der Leitfaden zum Lösen von Captchas in Hangfire-Jobs.

Kann der Worker auf einem anderen Rechner laufen als CapSkip?

Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse statt auf Loopback lauscht, und setzen Sie CAPSKIP_HOST auf diese Adresse. Der Worker ruft es dann über dieselbe HTTP-API von jedem Rechner aus auf, der den Port erreichen kann, mit einer Blockierregel in der Windows Firewall für die Adressen, die ihn nicht erreichen sollen. Der Solver bleibt auf Hardware, die Ihnen gehört, an der Art, wie Lösungen gezählt werden, ändert sich also nichts.

Die Kurzfassung

Ein Captcha-Job im Worker Service braucht drei Dinge, die Ihnen die Vorlage nicht liefert. Fangen Sie CapSkipError und die Exceptions der Website innerhalb der Schleife ab, denn eine einzige entkommene Exception stoppt den Host, und der Prozess endet mit Exit-Code 0. Übergeben Sie den Stopping-Token an RecaptchaAsync und filtern Sie jeden catch-Block darauf, damit ein Stopp einen Moment statt 30 Sekunden dauert und nie als Absturz im Log steht. Und beenden Sie wirklich unerwartete Fehlschläge mit Environment.Exit(1), damit die Wiederherstellungsoptionen des Dienstes ihn neu starten können. Versuchen Sie es alle 30 Sekunden erneut, solange CapSkip nicht läuft, und wechseln Sie in den Server-Modus, sobald der Worker den Rechner des Solvers verlässt.

Ein Worker, der monatelang alle 15 Minuten läuft, kommt auf Tausende Lösungen. Genau da zahlt sich eine Captcha-Umgehung auf Ihrem eigenen Windows-Rechner aus: Die Zahl der Lösungen erscheint nie auf einer Rechnung.