Как решать капчу в заданиях Windows Task Scheduler

task scheduler captcha - How to Solve CAPTCHAs in Windows Task Scheduler Jobs

Чтобы решать капчу из Windows Task Scheduler, зарегистрируйте задачу, которая напрямую запускает python.exe из вашего виртуального окружения, дайте скрипту собственные повторы и HTTP-таймауты, а всё остальное, что пойдёт не так, пусть ловит лимит времени задачи. Это и есть вся настройка решения капчи в Task Scheduler, а CapSkip работает на том же ПК, поэтому поднимать очередь не нужно и платы за каждое решение нет. Значения по умолчанию скрывают четыре ловушки, и в этом руководстве разобрана каждая. Рабочей папкой оказывается System32, поэтому относительное имя файла не работает. Один зависший запуск молча обрывает расписание. Обёртка cmd оставляет настоящий процесс Python работающим, когда планировщик убивает задачу. А настройка перезапуска при сбое не перезапускает скрипт, завершившийся с ошибкой.

Что понадобится

  • CapSkip, запущенный на ПК с Windows, с включённым решателем reCAPTCHA. CapSkip является настольным приложением, поэтому задача работает, только пока оно открыто.
  • Python 3.10 или новее: это минимальная версия для SDK CapSkip. Всё, что описано ниже, запускалось в Windows 11 с Python 3.14.0 и capskip 1.3.0, в Windows PowerShell 5.1.
  • Обычное окно PowerShell, а не запущенное с повышенными правами. Для регистрации и запуска задачи в тестах не потребовалось подтверждение прав администратора.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы задача на другой машине могла обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.

Шаг 1: создаём папку для задания и окружение Python

Поместите задание в одну папку вместе с собственным виртуальным окружением. Задача будет запускать python.exe из этого окружения по его полному пути, поэтому ей не нужны ни ваш PATH, ни активированная оболочка.

# PowerShell: make a job folder with its own Python environment
mkdir C:\CapSkipJobs
cd C:\CapSkipJobs
python -m venv venv
.\venv\Scripts\python.exe -m pip install capskip==1.3.0 requests

Версия SDK зафиксирована, потому что патч таймаута в шаге 2 обращается к внутренним частям SDK. Заново проверьте его, прежде чем переходить на более новую версию. Task Manager показывает по два процесса python.exe на каждый запуск: лаунчер окружения и настоящий интерпретатор, который он запускает. Это нормально.

Шаг 2: читаем настройки, пишем лог в файл и задаём SDK таймаут

Сохраните код из шагов со 2 по 4 в одном файле, C:\CapSkipJobs\job.py, в показанном порядке. Весь файл приведён ещё раз в конце руководства. Задание читает свои настройки из переменных окружения и само передаёт их клиенту, потому что SDK сам их не читает. Значения по умолчанию в нём тоже есть, поэтому файл запускается без какой-либо предварительной настройки. CAPSKIP_API_KEY нужен, только если вы включаете в CapSkip проверку API-ключа.

import functools
import logging
import os
import re
import sys
import time
import types

import requests
import capskip.api
from capskip import ApiException, CapSkip, NetworkException, TimeoutException

HOST = os.getenv('CAPSKIP_HOST', '127.0.0.1')
PORT = int(os.getenv('CAPSKIP_PORT', '8080'))
API_KEY = os.getenv('CAPSKIP_API_KEY', 'YOUR_API_KEY')
SITE = os.getenv('SITE_URL', 'https://example.com').rstrip('/')
USER = os.getenv('SITE_USER', 'YOUR_USERNAME')
PASSWORD = os.getenv('SITE_PASSWORD', 'YOUR_PASSWORD')
LOG_FILE = os.getenv('JOB_LOG', r'C:\CapSkipJobs\job.log')
ATTEMPTS = int(os.getenv('JOB_ATTEMPTS', '3'))
PAUSE = int(os.getenv('JOB_PAUSE', '30'))

В Task Scheduler нет поля для переменных окружения, а задача не видит переменную, заданную только в терминале. Переменная пользователя, заданная через setx, до задачи доходит, даже если её задали уже после регистрации: следующий запуск подхватит её без выхода из системы. Значения лежат в реестре вашей учётной записи открытым текстом и защищены примерно так же, как файл .env.

# Task Scheduler has no environment field. User variables work:
# the next run of the task sees them, with no sign-out needed.
setx SITE_URL "https://example.com"
setx SITE_USER "YOUR_USERNAME"
setx SITE_PASSWORD "YOUR_PASSWORD"

Дальше идут файл лога и таймаут для SDK. Task Scheduler отбрасывает вывод программы, поэтому print() нигде ничего не показывает, и задание вместо этого пишет лог в файл по абсолютному пути. Если рабочая папка не задана, задача запускается в C:\Windows\System32, где относительное имя файла вроде job.log вызывает PermissionError.

os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
                    format='%(asctime)s pid=%(process)d %(levelname)s %(message)s')
log = logging.getLogger('job')

# The SDK sets no HTTP timeouts, so give every CapSkip call one: connect 5 s, read 30 s.
# This reaches into the SDK, so the guard fails loudly if a newer version changes it.
if not hasattr(capskip.api, 'requests'):
    log.error('capskip.api.requests is gone: check the timeout patch')
    sys.exit(1)
capskip.api.requests = types.SimpleNamespace(
    RequestException=requests.RequestException,
    post=functools.partial(requests.post, timeout=(5, 30)),
    get=functools.partial(requests.get, timeout=(5, 30)))

Последние строки исправляют проблему, которую планировщик исправить не может. Python SDK CapSkip не задаёт HTTP-таймаут, поэтому если CapSkip принимает соединение, а потом так и не отвечает, вызов ждёт вечно, а настройка recaptchaTimeout учитывается только между опросами. Патч даёт каждому вызову SDK таймаут подключения в 5 секунд и таймаут чтения в 30 секунд. Запрос на отправку капчи, на который так и не приходит ответ, после этого падает с NetworkException через 30 секунд. Опрос результата, на который не приходит ответ, заканчивается исключением TimeoutException вскоре после того, как истёк recaptchaTimeout: через 333 секунды при значении по умолчанию 300. Защитная проверка над патчем останавливает задание и оставляет строку в логе, если в более новом SDK этого атрибута уже нет, вместо того чтобы позволить заданию работать без таймаутов. В руководстве по решению капчи в APScheduler тот же патч объяснён подробнее.

Шаг 3: загружаем форму, решаем капчу и отправляем её

Задание загружает страницу входа в Session из requests, читает с неё токен CSRF и sitekey и запрашивает у CapSkip токен. Одна и та же Session хранит cookie, который идёт вместе с токеном CSRF. Отправка формы передаёт и Referer, потому что некоторые фреймворки, например Django по HTTPS, отклоняют отправку формы без него. Замените пути и имена полей на те, которые использует ваш сайт.

def fetch_login(session):
    """Load the login page: the session cookie, the CSRF token and the sitekey."""
    page = session.get(SITE + '/login', timeout=(5, 30))
    if page.status_code != 200:
        raise RuntimeError('login page returned HTTP %d' % page.status_code)
    csrf = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', page.text)
    key = re.search(r'data-sitekey="([^"]+)"', page.text)
    if not csrf or not key:
        raise RuntimeError('login page has no CSRF token or no sitekey')
    return csrf.group(1), key.group(1)


def solve(sitekey):
    solver = CapSkip(apiKey=API_KEY, host=HOST, port=PORT)
    return solver.recaptcha(sitekey=sitekey, url=SITE + '/login')['code']

solver.recaptcha возвращает результат, в поле code которого лежит токен. Отправка ниже помещает его в поле g-recaptcha-response рядом с остальными полями формы. На неудачный вход сайт часто отвечает статусом 200 и той же формой входа, поэтому задание считает отказом и страницу, на которой всё ещё есть поле пароля. Измените эту проверку под свой сайт, потому что задание, которое сообщает об успехе при отклонённом входе, хуже задания, которое падает.

def submit(session, csrf, token):
    """Post the login form, the way the browser would, Referer included."""
    resp = session.post(SITE + '/login', timeout=(5, 30),
                        headers={'Referer': SITE + '/login'},
                        data={'csrfmiddlewaretoken': csrf, 'username': USER,
                              'password': PASSWORD, 'g-recaptcha-response': token})
    # Many sites answer a failed login with 200 and the form again.
    if resp.status_code != 200 or 'type="password"' in resp.text:
        raise RuntimeError('login rejected: HTTP %d %s' % (resp.status_code, resp.text[:120]))
    log.info('login accepted: %s', resp.text[:40])

Для других типов капчи вызовите соответствующий метод SDK и измените имя поля формы в отправляемом запросе. В руководстве по решению reCAPTCHA v2 на Python тот же вызов разобран для обычного скрипта.

Шаг 4: делаем повторы в скрипте и завершаем работу с кодом выхода

Заданию по расписанию нужен план на случай сбоя, и собственная настройка перезапуска в Task Scheduler таким планом не является. RestartCount и RestartInterval перезапускали задачу, только если она не смогла запуститься: отсутствующая программа запускалась повторно примерно через минуту и через две минуты. Скрипт, завершившийся с кодом 1 или 3 либо упавший с необработанным исключением, выполнялся один раз и больше не перезапускался. Поэтому повторы живут в самом скрипте.

def run():
    session = requests.Session()  # a fresh session means a fresh CSRF token
    csrf, sitekey = fetch_login(session)
    log.info('solving reCAPTCHA v2, sitekey %s', sitekey)
    submit(session, csrf, solve(sitekey))


def is_retryable(exc):
    if isinstance(exc, (NetworkException, TimeoutException, requests.RequestException)):
        return True
    return isinstance(exc, ApiException) and 'UNSOLVABLE' in str(exc)

Четыре сбоя стоят ещё одной попытки: CapSkip не отвечает, TimeoutException, ERROR_CAPTCHA_UNSOLVABLE (CapSkip не смог решить эту капчу, но новая попытка может это сделать) и сам сайт не отвечает. Всё остальное, например отклонённый вход или страница без токена CSRF, считается фатальным сбоем, потому что повтор привёл бы к тому же результату.

def main():
    log.info('start, CapSkip at %s:%s, site %s', HOST, PORT, SITE)
    for attempt in range(1, ATTEMPTS + 1):
        try:
            run()
            log.info('done, exit 0')
            return 0
        except Exception as exc:
            retry = is_retryable(exc)
            log.warning('attempt %d/%d failed, %s: %s: %s', attempt, ATTEMPTS,
                        'retryable' if retry else 'fatal', type(exc).__name__, exc,
                        exc_info=not retry)
            if not retry:
                return 1
            if attempt < ATTEMPTS:
                time.sleep(PAUSE)
    log.error('giving up after %d attempts, exit 3', ATTEMPTS)
    return 3


if __name__ == '__main__':
    sys.exit(main())

Задание делает до трёх попыток с паузой в 30 секунд между ними, а затем завершается с кодом, который Task Scheduler показывает как Last Run Result.

Код выходаЗначение
0Вход выполнен
1Сбой, который повтор не исправит; traceback есть в логе
3Каждая попытка закончилась сбоем, который может пройти сам

Код 2 намеренно пропущен. Сам Python завершается с кодом 2, когда не может открыть файл скрипта, поэтому 2 в Last Run Result был бы неоднозначным.

Тот же цикл справляется и с CapSkip, который ещё не открыт. Когда CapSkip закрыт, одна попытка падает примерно за 2 секунды. В тесте, где решатель и сайт оба поднялись с опозданием, первая попытка не удалась, а вторая, через 30 секунд, прошла успешно. Если вы хотите, чтобы задание запускалось при входе в систему, а не по расписанию, триггер входа для вашей собственной учётной записи регистрируется без прав администратора и допускает задержку; задержка в одну минуту зарегистрировалась без проблем.

Шаг 5: регистрируем задачу

Этот скрипт PowerShell регистрирует задачу для текущего пользователя. Для него не нужно окно с повышенными правами.

$python = 'C:\CapSkipJobs\venv\Scripts\python.exe'
$jobDir = 'C:\CapSkipJobs'

$action = New-ScheduledTaskAction -Execute $python `
    -Argument ('"{0}\job.py"' -f $jobDir) -WorkingDirectory $jobDir
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
    -RepetitionInterval (New-TimeSpan -Hours 1)
$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Minutes 30) `
    -MultipleInstances IgnoreNew `
    -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries

Register-ScheduledTask -TaskName 'CapSkip login job' -Action $action `
    -Trigger $trigger -Settings $settings -Force

Каждая часть здесь нужна не просто так.

  • Действие запускает python.exe из окружения напрямую: скрипт передаётся как абсолютный путь в кавычках, а папка задания назначена рабочей. Не ставьте перед ним cmd /c или файл .bat. Почему, показано в разделе о том, что на самом деле останавливает лимит времени.
  • Триггер срабатывает через минуту после регистрации, а затем каждый час, без даты окончания.
  • Лимит времени равен 30 минутам. По умолчанию он составляет 72 часа, то есть три дня. Дольше всего запуск может идти около 18 минут: три попытки, каждая из которых 333 секунды ждёт молчащий CapSkip, плюс две паузы по 30 секунд. Тридцать минут оставляют запас на это и всё равно освобождают застрявший запуск в то же утро.
  • IgnoreNew и так используется по умолчанию для нескольких экземпляров. Он указан явно, чтобы выбор был виден.
  • По умолчанию задача не запускается при питании от батареи и останавливается, когда ПК переключается на батарею. AllowStartIfOnBatteries и DontStopIfGoingOnBatteries снимают оба ограничения, и это важно для ноутбука. StartWhenAvailable позволяет планировщику запустить пропущенный запуск, например пока ПК спал, как только появится возможность. Все параметры Microsoft описывает в справочнике New-ScheduledTaskSettingsSet.
  • Задача выполняется от вашего имени и только пока вы вошли в систему. Это подходит для CapSkip, настольного приложения, которое работает в вашей сессии.

Остаётся ещё один выбор: python.exe или pythonw.exe. На ПК с Windows 11, который использовался для этих тестов, python.exe не открывал видимого окна, только скрытый хост консоли. У pythonw.exe консоли нет вообще, поэтому его sys.stdout и sys.stderr равны None: print() ничего не делает, sys.stderr.write выбрасывает AttributeError, а необработанное исключение не оставляет следов нигде. Задание пишет лог в файл, поэтому работает с любым из них. Переключайтесь на pythonw.exe, только если на вашем ПК появляется окно консоли.

Шаг 6: запускаем задачу и читаем результат

Блок ниже запускает задачу вручную, ждёт, пока она перестанет выполняться, а затем показывает, чем она закончилась и что записало в лог задание.

# Run the task now instead of waiting for the trigger
Start-ScheduledTask -TaskName 'CapSkip login job'
Start-Sleep -Seconds 2

# Wait until it is no longer running
while ((Get-ScheduledTask -TaskName 'CapSkip login job').State -eq 'Running') {
    Start-Sleep -Seconds 2
}

# How did it end, and what did the job write?
Get-ScheduledTaskInfo -TaskName 'CapSkip login job' |
    Select-Object LastRunTime, LastTaskResult, NextRunTime
Get-Content C:\CapSkipJobs\job.log -Tail 6

Last Run Result представляет собой число. Вот те значения, которые вам встретятся.

LastTaskResultHex-кодЗначение
00x0Задание завершилось с кодом 0
10x1Задание завершилось с кодом 1 или упало с необработанным исключением
30x3Задание завершилось с кодом 3 после всех попыток
2670090x41301Задача выполняется сейчас
2670110x41303Задача ещё не запускалась
2670140x41306Планировщик остановил её по лимиту времени или командой Stop-ScheduledTask
21479467200x800710E0Триггер проигнорирован, потому что предыдущий запуск ещё шёл
21479424020x80070002Путь к программе не существует

Почему один зависший запуск останавливает всё расписание

Это тот сбой, который значения по умолчанию делают хуже всего. По умолчанию для нескольких экземпляров действует политика IgnoreNew, поэтому триггер, сработавший, пока предыдущий запуск ещё идёт, отбрасывается. Лимит времени по умолчанию равен 72 часам. Вместе это означает, что один запуск, который никогда не заканчивается, блокирует расписание на три дня.

В тесте задание запускалось без патча таймаута против CapSkip, который принимал соединение и никогда не отвечал, при триггере каждую минуту. Задача оставалась в состоянии Running все пять минут наблюдения. В логе задания была одна строка о старте и ничего после неё. Тем временем LastRunTime сдвигался вперёд каждую минуту, а LastTaskResult сменился на 0x800710E0, код триггера, проигнорированного потому, что экземпляр ещё выполнялся. Задача выглядела живой, а число указывало на пропущенный триггер, а не на зависший запуск. Собственный журнал истории Task Scheduler по умолчанию отключён, поэтому лог вашего задания остаётся единственной записью, которая у вас будет.

Предотвращают это три уровня защиты, и каждый ловит то, что пропускают остальные. Таймаут SDK обрывает зависший вызов CapSkip за время от 30 до 333 секунд. Цикл повторов превращает это в ещё одну попытку или в чистый выход с кодом 3. Лимит времени служит страховкой на случай зависания, о котором вы не подумали. При всех трёх уровнях защиты CapSkip, который так и не ответил на отправку, приводил к завершению задания с кодом 3 примерно через 152 секунды: три таймаута по 30 секунд и две паузы. А CapSkip, который не отвечал на опросы результата, обходится до 333 секунд на попытку, и лимит в 30 минут это всё равно покрывает.

Что на самом деле останавливает лимит времени

Лимит времени работает точно. Лимит в одну минуту убил запуск на 60.2 секунды, а Task Scheduler принял даже лимит в 30 секунд и убил запуск на 30.5. Что именно будет убито, зависит от того, как устроено действие.

ДействиеУбито по лимитуПродолжает работать после этого
python.exe из окруженияЛаунчер и интерпретаторНичего
cmd /c python job.pycmd.exeЛаунчер и интерпретатор, всё ещё удерживающие соединение с CapSkip
Файл .bat, который запускает pythoncmd.exeЛаунчер и интерпретатор, всё ещё удерживающие соединение с CapSkip

То же разделение проявилось и при Stop-ScheduledTask. Для задания с капчей это важно, потому что осиротевший процесс держит соединение с CapSkip открытым. У обёрток есть и менее заметные ловушки. Строка cmd /c с перенаправлением требует лишней пары внешних кавычек, иначе задание завершается с кодом 1 и ничего не записывает в лог, а цель перенаправления должна быть абсолютным путём. Файл .bat, последняя строка которого echo, сообщает 0 независимо от того, что вернул Python, поэтому завершайте его строкой exit /b %ERRORLEVEL%. А Unregister-ScheduledTask не останавливает запуск, который уже идёт, поэтому сначала вызовите Stop-ScheduledTask.

Полное задание

Ниже все шесть блоков job.py собраны вместе. Сохраните файл как C:\CapSkipJobs\job.py, именно на этот путь указывает задача.

import functools
import logging
import os
import re
import sys
import time
import types

import requests
import capskip.api
from capskip import ApiException, CapSkip, NetworkException, TimeoutException

HOST = os.getenv('CAPSKIP_HOST', '127.0.0.1')
PORT = int(os.getenv('CAPSKIP_PORT', '8080'))
API_KEY = os.getenv('CAPSKIP_API_KEY', 'YOUR_API_KEY')
SITE = os.getenv('SITE_URL', 'https://example.com').rstrip('/')
USER = os.getenv('SITE_USER', 'YOUR_USERNAME')
PASSWORD = os.getenv('SITE_PASSWORD', 'YOUR_PASSWORD')
LOG_FILE = os.getenv('JOB_LOG', r'C:\CapSkipJobs\job.log')
ATTEMPTS = int(os.getenv('JOB_ATTEMPTS', '3'))
PAUSE = int(os.getenv('JOB_PAUSE', '30'))


os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
                    format='%(asctime)s pid=%(process)d %(levelname)s %(message)s')
log = logging.getLogger('job')

# The SDK sets no HTTP timeouts, so give every CapSkip call one: connect 5 s, read 30 s.
# This reaches into the SDK, so the guard fails loudly if a newer version changes it.
if not hasattr(capskip.api, 'requests'):
    log.error('capskip.api.requests is gone: check the timeout patch')
    sys.exit(1)
capskip.api.requests = types.SimpleNamespace(
    RequestException=requests.RequestException,
    post=functools.partial(requests.post, timeout=(5, 30)),
    get=functools.partial(requests.get, timeout=(5, 30)))


def fetch_login(session):
    """Load the login page: the session cookie, the CSRF token and the sitekey."""
    page = session.get(SITE + '/login', timeout=(5, 30))
    if page.status_code != 200:
        raise RuntimeError('login page returned HTTP %d' % page.status_code)
    csrf = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', page.text)
    key = re.search(r'data-sitekey="([^"]+)"', page.text)
    if not csrf or not key:
        raise RuntimeError('login page has no CSRF token or no sitekey')
    return csrf.group(1), key.group(1)


def solve(sitekey):
    solver = CapSkip(apiKey=API_KEY, host=HOST, port=PORT)
    return solver.recaptcha(sitekey=sitekey, url=SITE + '/login')['code']


def submit(session, csrf, token):
    """Post the login form, the way the browser would, Referer included."""
    resp = session.post(SITE + '/login', timeout=(5, 30),
                        headers={'Referer': SITE + '/login'},
                        data={'csrfmiddlewaretoken': csrf, 'username': USER,
                              'password': PASSWORD, 'g-recaptcha-response': token})
    # Many sites answer a failed login with 200 and the form again.
    if resp.status_code != 200 or 'type="password"' in resp.text:
        raise RuntimeError('login rejected: HTTP %d %s' % (resp.status_code, resp.text[:120]))
    log.info('login accepted: %s', resp.text[:40])


def run():
    session = requests.Session()  # a fresh session means a fresh CSRF token
    csrf, sitekey = fetch_login(session)
    log.info('solving reCAPTCHA v2, sitekey %s', sitekey)
    submit(session, csrf, solve(sitekey))


def is_retryable(exc):
    if isinstance(exc, (NetworkException, TimeoutException, requests.RequestException)):
        return True
    return isinstance(exc, ApiException) and 'UNSOLVABLE' in str(exc)


def main():
    log.info('start, CapSkip at %s:%s, site %s', HOST, PORT, SITE)
    for attempt in range(1, ATTEMPTS + 1):
        try:
            run()
            log.info('done, exit 0')
            return 0
        except Exception as exc:
            retry = is_retryable(exc)
            log.warning('attempt %d/%d failed, %s: %s: %s', attempt, ATTEMPTS,
                        'retryable' if retry else 'fatal', type(exc).__name__, exc,
                        exc_info=not retry)
            if not retry:
                return 1
            if attempt < ATTEMPTS:
                time.sleep(PAUSE)
    log.error('giving up after %d attempts, exit 3', ATTEMPTS)
    return 3


if __name__ == '__main__':
    sys.exit(main())

Запуск CapSkip на другой машине

Задаче и решателю не обязательно работать на одном ПК. Переключите CapSkip в режим Server в настройках подключения, и он будет слушать ваш сетевой адрес или публичный IP. Затем направьте на него задание. Строка setx ниже вступает в силу при следующем запуске.

# Only if CapSkip runs in Server mode on another Windows PC
setx CAPSKIP_HOST "192.168.1.50"
# Only if API key validation is switched on in CapSkip
setx CAPSKIP_API_KEY "YOUR_API_KEY"

Если маршрут идёт через интернет, используйте статический публичный IP и включите проверку API-ключа, указав тот же ключ в CAPSKIP_API_KEY. При запуске режима Server CapSkip открывает свой порт для всех адресов, поэтому, чтобы ограничить доступ ожидаемыми машинами, добавьте в Windows Firewall правило, блокирующее все остальные. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.

Частые ошибки при решении капчи в Task Scheduler

Что вы видитеПричинаИсправить
Состояние задачи часами остаётся RunningВызов завис, а лимит по умолчанию в 72 часа ещё далекоДобавьте таймаут SDK и лимит времени в несколько минут
2147946720 (0x800710E0) при каждом срабатывании триггераТриггеры игнорируются, пока ещё идёт один запускПрочитайте последнюю строку лога задания, затем устраните зависание
2147942402 (0x80070002)Путь к программе не существуетИспользуйте полный путь к python.exe из окружения
Last Run Result 2 и пустой логPython не смог открыть job.py, обычно из-за относительного пути без рабочей папкиЗадайте действию абсолютный путь к скрипту и рабочую папку
PermissionError на job.logОтносительное имя файла разрешилось в папку C:\Windows\System32Используйте абсолютные пути
Выход с кодом 3 при каждом запускеCapSkip не запущен, хост или порт указаны неверно, либо каждое решение закончилось сбоемПроверьте первую строку лога и адрес в настройках подключения
267014 (0x41306)Планировщик остановил запуск по лимиту времениНайдите, где обрывается лог, и исправьте этот вызов
Внутри задания не хватает настроекПеременные были заданы только в терминалеЗадайте их через setx или оставьте значения по умолчанию в файле
После остановки остаются два процесса python.exeВ качестве действия указан cmd /c или файл .batЗапускайте python.exe напрямую как действие

FAQ

Может ли задача выполняться, когда я вышел из системы?

Регистрация задачи, которая выполняется независимо от того, вошли вы в систему или нет, без сохранённого пароля, в тестах завершилась ошибкой Access is denied (0x80070005) в окне PowerShell без повышенных прав. Регистрацию с сохранённым паролем мы не проверяли, как и учётную запись обычного пользователя. В любом случае CapSkip должен быть открыт, чтобы отвечать, а это настольное приложение, поэтому обычная схема предполагает ПК, на котором вы остаётесь в системе. Если решатель нужен на машине, за которой никто не работает, запустите там CapSkip в режиме Server и направьте на него задание.

Может ли следующий триггер заменить зависший запуск, а не игнорироваться?

Да, с политикой StopExisting. New-ScheduledTaskSettingsSet предлагает только Parallel, Queue и IgnoreNew, поэтому задайте значение у объекта настроек вручную:

# New-ScheduledTaskSettingsSet cannot set this policy, so set it on the
# $settings object from Step 5 by hand, then update the task.
$settings.CimInstanceProperties['MultipleInstances'].Value = 3
Set-ScheduledTask -TaskName 'CapSkip login job' -Settings $settings

В экспортированной задаче после этого указано StopExisting. Когда действием был python.exe, каждый триггер убивал старый запуск и начинал новый. Когда действием был cmd /c, каждая такая остановка оставляла после себя осиротевший процесс. Parallel запускал новый экземпляр каждую минуту, пока зависший оставался на месте, а Queue ждал в очереди за ним.

Подходит ли это для скомпилированной программы на C#?

Сторона планировщика остаётся той же. Направьте действие на свой exe, задайте рабочую папку, сохраните лимит времени и используйте абсолютный путь к логу. Повторы и коды выхода тогда относятся к вашему собственному коду. Если программа должна работать весь день, а не запускаться по расписанию, то в руководстве по решению капчи в .NET Worker Service разобран именно такой вариант.

Когда очередь подходит лучше, чем Task Scheduler?

Когда работу создаёт другой код или когда каждая единица работы должна пережить падение и повторяться отдельно. Task Scheduler запускает новый процесс по расписанию и ничего не знает об отдельных заданиях. Очередь вроде Huey хранит каждую задачу в файле и повторяет её, а в руководстве по решению капчи в задачах Huey показана схема, которая работает в Windows с одним файлом SQLite.

Коротко

Заданию Task Scheduler для решения капчи нужно больше, чем само решение. Запускайте python.exe из окружения напрямую, с рабочей папкой, абсолютным путём к логу и абсолютным путём к скрипту. Задайте HTTP-вызовам SDK таймаут и делайте повторы внутри скрипта, потому что настройка перезапуска игнорирует коды выхода. Установите лимит времени выполнения в несколько минут, поскольку значение по умолчанию в 72 часа позволяет одному зависшему запуску блокировать все последующие триггеры.

Задание, которое запускается каждый час, делает около 720 решений в месяц. Когда локальный сервис распознавания капчи выполняется на вашей собственной машине с Windows, это число никогда не превращается в счёт.