{"id":26398,"date":"2026-10-10T20:25:19","date_gmt":"2026-10-10T20:25:19","guid":{"rendered":"https:\/\/capskip.com\/?p=26398"},"modified":"2026-10-10T20:25:19","modified_gmt":"2026-10-10T20:25:19","slug":"task-scheduler-captcha-job","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/task-scheduler-captcha-job\/","title":{"rendered":"\u5982\u4f55\u5728 Windows Task Scheduler \u4f5c\u4e1a\u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>To solve CAPTCHAs from Windows Task Scheduler, register a task that runs your virtual environment&#8217;s python.exe directly, give the script its own retries and HTTP timeouts, and let the task&#8217;s time limit catch whatever else goes wrong. That is the whole task scheduler captcha setup, and CapSkip runs on the same PC, so there is no queue to host and no per-solve cost. The defaults hide four traps, and this guide covers each one. The working folder is System32, so a relative file name fails. One hung run silently ends the schedule. A cmd wrapper leaves the real Python process running when the scheduler kills the task. And the restart-on-failure setting does not restart a script that exits with an error.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip running on a Windows PC, with the reCAPTCHA solver switched on. CapSkip is a desktop app, so the task only works while it is open.<\/li>\n<li>Python 3.10 or later, the floor of the CapSkip SDK. Everything below was run on Windows 11 with Python 3.14.0 and capskip 1.3.0, in Windows PowerShell 5.1.<\/li>\n<li>An ordinary PowerShell prompt, not an elevated one. Registering and running the task needed no administrator prompt in testing.<\/li>\n<li>An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a task on another machine can call it over the API. Both are set under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: create a job folder and a Python environment<\/h2>\n<p>Put the job in one folder with its own virtual environment. The task will run the python.exe inside that environment by its full path, so it needs nothing from your PATH and no activated shell.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># PowerShell: make a job folder with its own Python environment\nmkdir C:\\CapSkipJobs\ncd C:\\CapSkipJobs\npython -m venv venv\n.\\venv\\Scripts\\python.exe -m pip install capskip==1.3.0 requests<\/pre>\n<\/div>\n<p>The SDK version is pinned because the timeout patch in Step 2 reaches into the SDK&#8217;s internals. Test it again before you move to a newer version. Task Manager shows two python.exe processes for every run: the environment&#8217;s launcher and the real interpreter it starts. That is normal.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: read settings, log to a file and give the SDK a timeout<\/h2>\n<p>Save the code from Steps 2 to 4 in one file, C:\\CapSkipJobs\\job.py, in the order shown. The whole file is repeated at the end of this guide. The job reads its settings from environment variables and passes them to the client itself, because the SDK does not read them on its own. It also has defaults, so the file runs without any setup. CAPSKIP_API_KEY is only needed if you switch on API key validation in CapSkip.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import functools\nimport logging\nimport os\nimport re\nimport sys\nimport time\nimport types\n\nimport requests\nimport capskip.api\nfrom capskip import ApiException, CapSkip, NetworkException, TimeoutException\n\nHOST = os.getenv('CAPSKIP_HOST', '127.0.0.1')\nPORT = int(os.getenv('CAPSKIP_PORT', '8080'))\nAPI_KEY = os.getenv('CAPSKIP_API_KEY', 'YOUR_API_KEY')\nSITE = os.getenv('SITE_URL', 'https:\/\/example.com').rstrip('\/')\nUSER = os.getenv('SITE_USER', 'YOUR_USERNAME')\nPASSWORD = os.getenv('SITE_PASSWORD', 'YOUR_PASSWORD')\nLOG_FILE = os.getenv('JOB_LOG', r'C:\\CapSkipJobs\\job.log')\nATTEMPTS = int(os.getenv('JOB_ATTEMPTS', '3'))\nPAUSE = int(os.getenv('JOB_PAUSE', '30'))<\/pre>\n<\/div>\n<p>Task Scheduler has no field for environment variables, and a task does not see a variable you set only in a terminal. A user variable set with setx does reach it, even one set after the task was registered: the next run picks it up, with no sign-out. The values sit in your account&#8217;s registry as plain text, about as private as a .env file.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Task Scheduler has no environment field. User variables work:\n# the next run of the task sees them, with no sign-out needed.\nsetx SITE_URL &quot;https:\/\/example.com&quot;\nsetx SITE_USER &quot;YOUR_USERNAME&quot;\nsetx SITE_PASSWORD &quot;YOUR_PASSWORD&quot;<\/pre>\n<\/div>\n<p>Next come the log file and a timeout for the SDK. Task Scheduler throws away a program&#8217;s output, so print() shows nowhere, and the job writes a log file at an absolute path instead. With no working folder set, the task runs in C:\\Windows\\System32, where a relative file name such as job.log fails with a PermissionError.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)\nlogging.basicConfig(filename=LOG_FILE, level=logging.INFO,\n                    format='%(asctime)s pid=%(process)d %(levelname)s %(message)s')\nlog = logging.getLogger('job')\n\n# The SDK sets no HTTP timeouts, so give every CapSkip call one: connect 5 s, read 30 s.\n# This reaches into the SDK, so the guard fails loudly if a newer version changes it.\nif not hasattr(capskip.api, 'requests'):\n    log.error('capskip.api.requests is gone: check the timeout patch')\n    sys.exit(1)\ncapskip.api.requests = types.SimpleNamespace(\n    RequestException=requests.RequestException,\n    post=functools.partial(requests.post, timeout=(5, 30)),\n    get=functools.partial(requests.get, timeout=(5, 30)))<\/pre>\n<\/div>\n<p>The last lines fix a problem the scheduler cannot. The CapSkip Python SDK sets no HTTP timeout, so if CapSkip accepts a connection and then never answers, the call waits forever, and the recaptchaTimeout setting only counts between polls. The patch gives every SDK call a 5 second connect timeout and a 30 second read timeout. A submit that never gets an answer then fails as a NetworkException after 30 seconds. A result poll that never gets an answer ends as a TimeoutException shortly after recaptchaTimeout is over: 333 seconds with its default of 300. The guard above the patch stops the job with a line in the log if a newer SDK no longer has that attribute, instead of letting it run without timeouts. The <a href=\"https:\/\/capskip.com\/apscheduler-captcha-job\/\">guide to solving CAPTCHAs in APScheduler<\/a> explains the same patch in more detail.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: fetch the form, solve it and post it<\/h2>\n<p>The job loads the login page in a requests Session, reads the CSRF token and the sitekey from it, and asks CapSkip for a token. One Session carries the cookie that goes with the CSRF token. The post sends the Referer too, because some frameworks, such as Django over HTTPS, reject a form post without one. Replace the paths and field names with the ones your site uses.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def fetch_login(session):\n    &quot;&quot;&quot;Load the login page: the session cookie, the CSRF token and the sitekey.&quot;&quot;&quot;\n    page = session.get(SITE + '\/login', timeout=(5, 30))\n    if page.status_code != 200:\n        raise RuntimeError('login page returned HTTP %d' % page.status_code)\n    csrf = re.search(r'name=&quot;csrfmiddlewaretoken&quot; value=&quot;([^&quot;]+)&quot;', page.text)\n    key = re.search(r'data-sitekey=&quot;([^&quot;]+)&quot;', page.text)\n    if not csrf or not key:\n        raise RuntimeError('login page has no CSRF token or no sitekey')\n    return csrf.group(1), key.group(1)\n\n\ndef solve(sitekey):\n    solver = CapSkip(apiKey=API_KEY, host=HOST, port=PORT)\n    return solver.recaptcha(sitekey=sitekey, url=SITE + '\/login')['code']<\/pre>\n<\/div>\n<p>solver.recaptcha returns a result whose code field is the token. The post below puts it in the g-recaptcha-response field next to the other form fields. A failed login often answers with status 200 and the login form again, so the job also treats a page that still has a password field as a rejection. Change that test to fit your site, because a job that reports success for a rejected login is worse than one that fails.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def submit(session, csrf, token):\n    &quot;&quot;&quot;Post the login form, the way the browser would, Referer included.&quot;&quot;&quot;\n    resp = session.post(SITE + '\/login', timeout=(5, 30),\n                        headers={'Referer': SITE + '\/login'},\n                        data={'csrfmiddlewaretoken': csrf, 'username': USER,\n                              'password': PASSWORD, 'g-recaptcha-response': token})\n    # Many sites answer a failed login with 200 and the form again.\n    if resp.status_code != 200 or 'type=&quot;password&quot;' in resp.text:\n        raise RuntimeError('login rejected: HTTP %d %s' % (resp.status_code, resp.text[:120]))\n    log.info('login accepted: %s', resp.text[:40])<\/pre>\n<\/div>\n<p>For other CAPTCHA types, call the matching SDK method and change the form field name in the post. The <a href=\"https:\/\/capskip.com\/recaptcha-v2-python\/\">guide to solving reCAPTCHA v2 in Python<\/a> covers the same call in a plain script.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: retry in the script and exit with a code<\/h2>\n<p>A scheduled job needs a plan for failure, and Task Scheduler&#8217;s own restart setting is not one. RestartCount and RestartInterval restarted a task only when it failed to start: a missing program was started again about one and two minutes later. A script that exited with code 1 or 3, or died with an uncaught exception, ran once and was never restarted. So the retry lives in the script.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def run():\n    session = requests.Session()  # a fresh session means a fresh CSRF token\n    csrf, sitekey = fetch_login(session)\n    log.info('solving reCAPTCHA v2, sitekey %s', sitekey)\n    submit(session, csrf, solve(sitekey))\n\n\ndef is_retryable(exc):\n    if isinstance(exc, (NetworkException, TimeoutException, requests.RequestException)):\n        return True\n    return isinstance(exc, ApiException) and 'UNSOLVABLE' in str(exc)<\/pre>\n<\/div>\n<p>Four failures are worth another attempt: CapSkip not answering, a TimeoutException, ERROR_CAPTCHA_UNSOLVABLE (CapSkip could not solve this task, and a fresh try can), and the site itself not answering. Anything else, such as a rejected login or a page with no CSRF token, is fatal, because a retry would repeat it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def main():\n    log.info('start, CapSkip at %s:%s, site %s', HOST, PORT, SITE)\n    for attempt in range(1, ATTEMPTS + 1):\n        try:\n            run()\n            log.info('done, exit 0')\n            return 0\n        except Exception as exc:\n            retry = is_retryable(exc)\n            log.warning('attempt %d\/%d failed, %s: %s: %s', attempt, ATTEMPTS,\n                        'retryable' if retry else 'fatal', type(exc).__name__, exc,\n                        exc_info=not retry)\n            if not retry:\n                return 1\n            if attempt &lt; ATTEMPTS:\n                time.sleep(PAUSE)\n    log.error('giving up after %d attempts, exit 3', ATTEMPTS)\n    return 3\n\n\nif __name__ == '__main__':\n    sys.exit(main())<\/pre>\n<\/div>\n<p>The job makes up to three attempts, 30 seconds apart, and then exits with a code that Task Scheduler shows as the Last Run Result.<\/p>\n<table>\n<thead>\n<tr>\n<th>Exit code<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>0<\/td>\n<td>The login went through<\/td>\n<\/tr>\n<tr>\n<td>1<\/td>\n<td>A failure a retry cannot fix; the log has the traceback<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>Every attempt hit a failure that may clear up on its own<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Code 2 is left out on purpose. Python itself exits with 2 when it cannot open the script file, so a 2 in the Last Run Result would be ambiguous.<\/p>\n<p>The same loop covers a CapSkip that is not open yet. With CapSkip closed, one attempt fails in about 2 seconds. In a test where the solver and the site both came up late, the first attempt failed and the second, 30 seconds later, succeeded. If you want the job to start at sign-in instead of on a clock, a log-on trigger for your own account registers without administrator rights and accepts a delay; a one-minute delay registered fine.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: register the task<\/h2>\n<p>This PowerShell script registers the task for the current user. Nothing in it needs an elevated prompt.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">$python = 'C:\\CapSkipJobs\\venv\\Scripts\\python.exe'\n$jobDir = 'C:\\CapSkipJobs'\n\n$action = New-ScheduledTaskAction -Execute $python `\n    -Argument ('&quot;{0}\\job.py&quot;' -f $jobDir) -WorkingDirectory $jobDir\n$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `\n    -RepetitionInterval (New-TimeSpan -Hours 1)\n$settings = New-ScheduledTaskSettingsSet `\n    -ExecutionTimeLimit (New-TimeSpan -Minutes 30) `\n    -MultipleInstances IgnoreNew `\n    -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries\n\nRegister-ScheduledTask -TaskName 'CapSkip login job' -Action $action `\n    -Trigger $trigger -Settings $settings -Force<\/pre>\n<\/div>\n<p>Each part earns its place.<\/p>\n<ul>\n<li>The action runs the environment&#8217;s python.exe directly, with the script as a quoted absolute path and the job folder as the working directory. Do not put cmd \/c or a .bat file in front of it. The section on what the time limit really stops shows why.<\/li>\n<li>The trigger fires one minute from now and then every hour, with no end date.<\/li>\n<li>The time limit is 30 minutes. The default is 72 hours, three days. The longest a run can take works out to about 18 minutes: three attempts that each wait out a silent CapSkip for 333 seconds, plus two 30 second pauses. Thirty minutes leaves room for that and still frees a stuck run the same morning.<\/li>\n<li>IgnoreNew is already the default for multiple instances. It is written out so the choice is visible.<\/li>\n<li>By default a task does not start on battery power and stops when the PC switches to it. AllowStartIfOnBatteries and DontStopIfGoingOnBatteries turn both off, which matters on a laptop. StartWhenAvailable lets the scheduler start a run it missed, for example while the PC slept, as soon as it can. Microsoft documents every option in the <a href=\"https:\/\/learn.microsoft.com\/en-us\/powershell\/module\/scheduledtasks\/new-scheduledtasksettingsset\" rel=\"nofollow noopener\" target=\"_blank\">New-ScheduledTaskSettingsSet reference<\/a>.<\/li>\n<li>The task runs as you, only while you are signed in. That suits CapSkip, which is a desktop app running in your session.<\/li>\n<\/ul>\n<p>One more choice is python.exe or pythonw.exe. On the Windows 11 PC used for these tests, python.exe opened no visible window, only a hidden console host. pythonw.exe has no console at all, so its sys.stdout and sys.stderr are None: print() does nothing, sys.stderr.write raises an AttributeError, and an uncaught exception leaves no trace anywhere. The job logs to a file, so it works under either. Switch to pythonw.exe only if a console window appears on your PC.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 6: run it and read the result<\/h2>\n<p>The block below starts the task by hand, waits until it is no longer running, then shows how it ended and what the job logged.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Run the task now instead of waiting for the trigger\nStart-ScheduledTask -TaskName 'CapSkip login job'\nStart-Sleep -Seconds 2\n\n# Wait until it is no longer running\nwhile ((Get-ScheduledTask -TaskName 'CapSkip login job').State -eq 'Running') {\n    Start-Sleep -Seconds 2\n}\n\n# How did it end, and what did the job write?\nGet-ScheduledTaskInfo -TaskName 'CapSkip login job' |\n    Select-Object LastRunTime, LastTaskResult, NextRunTime\nGet-Content C:\\CapSkipJobs\\job.log -Tail 6<\/pre>\n<\/div>\n<p>The Last Run Result is a number. These are the ones you will meet.<\/p>\n<table>\n<thead>\n<tr>\n<th>LastTaskResult<\/th>\n<th>Hex<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>0<\/td>\n<td>0x0<\/td>\n<td>The job exited 0<\/td>\n<\/tr>\n<tr>\n<td>1<\/td>\n<td>0x1<\/td>\n<td>The job exited 1, or died with an uncaught exception<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>0x3<\/td>\n<td>The job exited 3 after its attempts<\/td>\n<\/tr>\n<tr>\n<td>267009<\/td>\n<td>0x41301<\/td>\n<td>The task is running now<\/td>\n<\/tr>\n<tr>\n<td>267011<\/td>\n<td>0x41303<\/td>\n<td>The task has not run yet<\/td>\n<\/tr>\n<tr>\n<td>267014<\/td>\n<td>0x41306<\/td>\n<td>The scheduler stopped it, at the time limit or with Stop-ScheduledTask<\/td>\n<\/tr>\n<tr>\n<td>2147946720<\/td>\n<td>0x800710E0<\/td>\n<td>A trigger was ignored because a run was still going<\/td>\n<\/tr>\n<tr>\n<td>2147942402<\/td>\n<td>0x80070002<\/td>\n<td>The program path does not exist<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why one hung run stops the whole schedule<\/h2>\n<p>This is the failure the defaults make worst. The default policy for multiple instances is IgnoreNew, so a trigger that fires while the previous run is still going is dropped. The default time limit is 72 hours. Together they mean that one run that never ends blocks the schedule for three days.<\/p>\n<p>In a test, the job ran without the timeout patch against a CapSkip that accepted the connection and never answered, with a trigger every minute. The task stayed in the Running state for the five minutes watched. The job log held one start line and nothing after it. Meanwhile LastRunTime moved forward every minute and LastTaskResult changed to 0x800710E0, the code for a trigger ignored because an instance was still running. The task looked alive, and the number pointed at the skipped trigger rather than at the stuck run. Task Scheduler&#8217;s own history log is switched off by default, so your job log is the only record you will have.<\/p>\n<p>Three layers prevent it, and each catches something the others miss. The SDK timeout ends a hung CapSkip call in 30 to 333 seconds. The retry loop turns that into another attempt or a clean exit 3. The time limit is the backstop for the hang you did not think of. With all three in place, a CapSkip that never answered the submit ended the job with exit 3 after about 152 seconds: three 30 second timeouts and two pauses. One that never answered the result polls costs up to 333 seconds per attempt, which the 30 minute limit still covers.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What the time limit really stops<\/h2>\n<p>The time limit is precise. A limit of one minute killed the run at 60.2 seconds, and Task Scheduler accepted a 30 second limit and killed at 30.5. What it kills depends on the shape of the action.<\/p>\n<table>\n<thead>\n<tr>\n<th>Action<\/th>\n<th>Killed at the limit<\/th>\n<th>Still running afterwards<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The environment&#8217;s python.exe<\/td>\n<td>The launcher and the interpreter<\/td>\n<td>Nothing<\/td>\n<\/tr>\n<tr>\n<td>cmd \/c python job.py<\/td>\n<td>cmd.exe<\/td>\n<td>The launcher and the interpreter, still holding their connection to CapSkip<\/td>\n<\/tr>\n<tr>\n<td>A .bat file that runs python<\/td>\n<td>cmd.exe<\/td>\n<td>The launcher and the interpreter, still holding their connection to CapSkip<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The same split showed up with Stop-ScheduledTask. It matters for a CAPTCHA job because an orphaned process keeps its connection to CapSkip open. Wrappers have smaller traps too. A cmd \/c line with a redirect needs an extra pair of outer quotes, or the job exits 1 with nothing logged, and the redirect target must be an absolute path. A .bat file whose last line is echo reports 0 whatever Python returned, so end it with exit \/b %ERRORLEVEL%. And Unregister-ScheduledTask does not stop a run that is in progress, so call Stop-ScheduledTask first.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The full job<\/h2>\n<p>Here are the six blocks of job.py together. Save it as C:\\CapSkipJobs\\job.py, which is the path the task points at.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import functools\nimport logging\nimport os\nimport re\nimport sys\nimport time\nimport types\n\nimport requests\nimport capskip.api\nfrom capskip import ApiException, CapSkip, NetworkException, TimeoutException\n\nHOST = os.getenv('CAPSKIP_HOST', '127.0.0.1')\nPORT = int(os.getenv('CAPSKIP_PORT', '8080'))\nAPI_KEY = os.getenv('CAPSKIP_API_KEY', 'YOUR_API_KEY')\nSITE = os.getenv('SITE_URL', 'https:\/\/example.com').rstrip('\/')\nUSER = os.getenv('SITE_USER', 'YOUR_USERNAME')\nPASSWORD = os.getenv('SITE_PASSWORD', 'YOUR_PASSWORD')\nLOG_FILE = os.getenv('JOB_LOG', r'C:\\CapSkipJobs\\job.log')\nATTEMPTS = int(os.getenv('JOB_ATTEMPTS', '3'))\nPAUSE = int(os.getenv('JOB_PAUSE', '30'))\n\n\nos.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)\nlogging.basicConfig(filename=LOG_FILE, level=logging.INFO,\n                    format='%(asctime)s pid=%(process)d %(levelname)s %(message)s')\nlog = logging.getLogger('job')\n\n# The SDK sets no HTTP timeouts, so give every CapSkip call one: connect 5 s, read 30 s.\n# This reaches into the SDK, so the guard fails loudly if a newer version changes it.\nif not hasattr(capskip.api, 'requests'):\n    log.error('capskip.api.requests is gone: check the timeout patch')\n    sys.exit(1)\ncapskip.api.requests = types.SimpleNamespace(\n    RequestException=requests.RequestException,\n    post=functools.partial(requests.post, timeout=(5, 30)),\n    get=functools.partial(requests.get, timeout=(5, 30)))\n\n\ndef fetch_login(session):\n    &quot;&quot;&quot;Load the login page: the session cookie, the CSRF token and the sitekey.&quot;&quot;&quot;\n    page = session.get(SITE + '\/login', timeout=(5, 30))\n    if page.status_code != 200:\n        raise RuntimeError('login page returned HTTP %d' % page.status_code)\n    csrf = re.search(r'name=&quot;csrfmiddlewaretoken&quot; value=&quot;([^&quot;]+)&quot;', page.text)\n    key = re.search(r'data-sitekey=&quot;([^&quot;]+)&quot;', page.text)\n    if not csrf or not key:\n        raise RuntimeError('login page has no CSRF token or no sitekey')\n    return csrf.group(1), key.group(1)\n\n\ndef solve(sitekey):\n    solver = CapSkip(apiKey=API_KEY, host=HOST, port=PORT)\n    return solver.recaptcha(sitekey=sitekey, url=SITE + '\/login')['code']\n\n\ndef submit(session, csrf, token):\n    &quot;&quot;&quot;Post the login form, the way the browser would, Referer included.&quot;&quot;&quot;\n    resp = session.post(SITE + '\/login', timeout=(5, 30),\n                        headers={'Referer': SITE + '\/login'},\n                        data={'csrfmiddlewaretoken': csrf, 'username': USER,\n                              'password': PASSWORD, 'g-recaptcha-response': token})\n    # Many sites answer a failed login with 200 and the form again.\n    if resp.status_code != 200 or 'type=&quot;password&quot;' in resp.text:\n        raise RuntimeError('login rejected: HTTP %d %s' % (resp.status_code, resp.text[:120]))\n    log.info('login accepted: %s', resp.text[:40])\n\n\ndef run():\n    session = requests.Session()  # a fresh session means a fresh CSRF token\n    csrf, sitekey = fetch_login(session)\n    log.info('solving reCAPTCHA v2, sitekey %s', sitekey)\n    submit(session, csrf, solve(sitekey))\n\n\ndef is_retryable(exc):\n    if isinstance(exc, (NetworkException, TimeoutException, requests.RequestException)):\n        return True\n    return isinstance(exc, ApiException) and 'UNSOLVABLE' in str(exc)\n\n\ndef main():\n    log.info('start, CapSkip at %s:%s, site %s', HOST, PORT, SITE)\n    for attempt in range(1, ATTEMPTS + 1):\n        try:\n            run()\n            log.info('done, exit 0')\n            return 0\n        except Exception as exc:\n            retry = is_retryable(exc)\n            log.warning('attempt %d\/%d failed, %s: %s: %s', attempt, ATTEMPTS,\n                        'retryable' if retry else 'fatal', type(exc).__name__, exc,\n                        exc_info=not retry)\n            if not retry:\n                return 1\n            if attempt &lt; ATTEMPTS:\n                time.sleep(PAUSE)\n    log.error('giving up after %d attempts, exit 3', ATTEMPTS)\n    return 3\n\n\nif __name__ == '__main__':\n    sys.exit(main())<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running CapSkip on another machine<\/h2>\n<p>The task and the solver do not have to share a PC. Switch CapSkip to Server mode under connection settings, and it listens on your network address or public IP. Then point the job at it. The setx line below takes effect on the next run.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Only if CapSkip runs in Server mode on another Windows PC\nsetx CAPSKIP_HOST &quot;192.168.1.50&quot;\n# Only if API key validation is switched on in CapSkip\nsetx CAPSKIP_API_KEY &quot;YOUR_API_KEY&quot;<\/pre>\n<\/div>\n<p>Use a static public IP if the route crosses the internet, and turn on API key validation, with the same key in CAPSKIP_API_KEY. CapSkip opens its port to every address when Server mode starts, so to limit access to the machines you expect, add a Windows Firewall block rule for the others. The solver is still your own Windows machine, and solves are still unmetered.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common Task Scheduler CAPTCHA errors<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The task state stays Running for hours<\/td>\n<td>A call is stuck and the 72 hour default limit is far away<\/td>\n<td>Add the SDK timeout and a time limit of minutes<\/td>\n<\/tr>\n<tr>\n<td>2147946720 (0x800710E0) at every trigger<\/td>\n<td>Triggers are being ignored while one run is still going<\/td>\n<td>Read the last line of the job log, then fix the hang<\/td>\n<\/tr>\n<tr>\n<td>2147942402 (0x80070002)<\/td>\n<td>The program path does not exist<\/td>\n<td>Use the full path to the environment&#8217;s python.exe<\/td>\n<\/tr>\n<tr>\n<td>Last Run Result 2 and an empty log<\/td>\n<td>Python could not open job.py, usually a relative path with no working folder<\/td>\n<td>Give the action an absolute script path and a working folder<\/td>\n<\/tr>\n<tr>\n<td>PermissionError on job.log<\/td>\n<td>A relative file name resolved inside C:\\Windows\\System32<\/td>\n<td>Use absolute paths<\/td>\n<\/tr>\n<tr>\n<td>Exit 3 on every run<\/td>\n<td>CapSkip is not running, the host or port is wrong, or every solve failed<\/td>\n<td>Check the first line of the log and the address under connection settings<\/td>\n<\/tr>\n<tr>\n<td>267014 (0x41306)<\/td>\n<td>The scheduler stopped the run at the time limit<\/td>\n<td>Find where the log ends, and fix that call<\/td>\n<\/tr>\n<tr>\n<td>Settings are missing inside the job<\/td>\n<td>The variables were set only in a terminal<\/td>\n<td>Set them with setx, or keep defaults in the file<\/td>\n<\/tr>\n<tr>\n<td>Two python.exe processes remain after a stop<\/td>\n<td>The action is cmd \/c or a .bat file<\/td>\n<td>Run python.exe as the action<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can the task run when I am signed out?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Registering a task that runs whether you are signed in or not, with no stored password, failed in testing with Access is denied (0x80070005) from a PowerShell prompt that was not elevated. Registering one with a stored password was not tested, and neither was a standard user account. Either way CapSkip has to be open to answer, and it is a desktop app, so the usual setup is a PC where you stay signed in. If you need the solver on a machine nobody uses, run CapSkip there in Server mode and point the job at it.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can the next trigger replace a hung run instead of being ignored?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, with the StopExisting policy. New-ScheduledTaskSettingsSet only offers Parallel, Queue and IgnoreNew, so set the value on the settings object by hand:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># New-ScheduledTaskSettingsSet cannot set this policy, so set it on the\n# $settings object from Step 5 by hand, then update the task.\n$settings.CimInstanceProperties['MultipleInstances'].Value = 3\nSet-ScheduledTask -TaskName 'CapSkip login job' -Settings $settings<\/pre>\n<\/div>\n<p style=\"margin:12px 0 0;\">The exported task then reads StopExisting. With python.exe as the action, each trigger killed the old run and started a fresh one. With cmd \/c as the action, every kill left an orphan behind. Parallel started a new run every minute while the hung one stayed, and Queue kept waiting behind it.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does this work for a compiled C# program?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The scheduler side is the same. Point the action at your exe, set the working folder, keep the time limit and use an absolute log path. The retries and exit codes then belong in your own code. If the program should run all day instead of being started on a clock, the <a href=\"https:\/\/capskip.com\/worker-service-captcha\/\">guide to solving CAPTCHAs in a .NET Worker Service<\/a> covers that shape.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">When is a queue a better fit than Task Scheduler?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">When other code creates the work, or when each piece of work must survive a crash and be retried on its own. Task Scheduler starts a fresh process on a clock and knows nothing about individual jobs. A queue such as Huey keeps every task in a file and retries it, and the <a href=\"https:\/\/capskip.com\/huey-captcha-task\/\">guide to solving CAPTCHAs in Huey tasks<\/a> shows a setup that runs on Windows with a single SQLite file.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>A Task Scheduler CAPTCHA job needs more than the solve. Run the environment&#8217;s python.exe directly, with a working folder, an absolute log path and an absolute script path. Give the SDK&#8217;s HTTP calls a timeout and retry inside the script, because the restart setting ignores exit codes. Set an execution time limit of minutes, since the default of 72 hours lets one hung run block every later trigger.<\/p>\n<ul>\n<li>Every CAPTCHA type the Python package handles: <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/li>\n<li>How the checkbox reCAPTCHA works and what the solver covers: <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<\/ul>\n<p>A job that runs every hour makes about 720 solves a month. Running them through a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a> on your own Windows machine means that number never turns into a bill.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u4e00\u6b21\u5361\u4f4f\u7684\u9a8c\u8bc1\u7801\u8bc6\u522b\uff0c\u5c31\u53ef\u80fd\u8ba9 Windows Task Scheduler \u7684\u8ba1\u5212\u6084\u65e0\u58f0\u606f\u5730\u505c\u6446\u4e09\u5929\u3002\u672c\u6587\u7ed9\u51fa\u4e00\u4e2a Python \u4f5c\u4e1a\uff1a\u5b83\u7528 CapSkip \u8bc6\u522b reCAPTCHA\uff0c\u5728\u811a\u672c\u91cc\u91cd\u8bd5\uff0c\u5e76\u6ce8\u518c\u4e00\u4e2a\u6302\u8d77\u7684\u8fd0\u884c\u4e5f\u963b\u585e\u4e0d\u4e86\u7684\u4efb\u52a1\u3002<\/p>","protected":false},"author":1,"featured_media":26397,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Task Scheduler CAPTCHA Job on Windows (Python) | CapSkip","rank_math_description":"Build a task scheduler captcha job in Python: solve reCAPTCHA with CapSkip, retry in the script, and register a task that a hung run cannot block.","rank_math_focus_keyword":"task scheduler captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-26398","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26398","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=26398"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26398\/revisions"}],"predecessor-version":[{"id":26403,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26398\/revisions\/26403"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/26397"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=26398"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=26398"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=26398"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}