> For the complete documentation index, see [llms.txt](https://docs.iexexchanger.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.iexexchanger.com/server-i-dannye/planirovshik-zadach.md).

# Планировщик задач

Настройка заданий cron на сервере

Планировщик задач запускает внутренние процессы iEXExchanger по установленному расписанию. Для этого системная служба CRON каждую минуту вызывает Laravel Scheduler, а система определяет, какие задачи должны быть выполнены в этот момент.

В штатной установке на чистом сервере используются три компонента:

| Компонент                            | Назначение                              | Нормальное состояние          |
| ------------------------------------ | --------------------------------------- | ----------------------------- |
| `cron.service`                       | Проверяет расписание каждую минуту      | `enabled` и `active`          |
| `/etc/cron.d/iexexchanger-scheduler` | Содержит единственную команду запуска   | Одна активная строка          |
| `iex-scheduler.service`              | Выполняет безопасный проверочный запуск | `inactive` и `Result=success` |

Планировщик запускается от отдельного пользователя приложения. Запуск от `root` запрещён: он может изменить владельцев файлов и нарушить работу сайта, обновлений и фоновых процессов.

{% hint style="info" %}
`iex-scheduler.service` имеет тип `oneshot`: он запускает команду, дожидается её завершения и останавливается. Поэтому состояние `inactive (dead)` после успешного запуска является правильным. Постоянно активной должна оставаться служба `cron.service`.
{% endhint %}

{% hint style="warning" %}
Инструкция предназначена для iEXExchanger, установленного на чистый сервер с помощью текущего Installer.

Если продукт работает через FASTPANEL, не выполняйте приведённые ниже команды. В старой конфигурации используются другие пользователи, пути и PHP. Ручное добавление нового планировщика может привести к двойному запуску задач. Для такой установки рекомендуется переход на чистый сервер.
{% endhint %}

***

## Какой способ восстановления выбрать

Сначала определите, завершилась ли первоначальная установка продукта.

{% tabs %}
{% tab title="Установка не завершилась" %}
Если Installer остановился на настройке или проверке планировщика, не создавайте системные файлы вручную. Продолжите тот же запуск с теми же файлами и конфигурацией.

Проверьте состояние установки:

{% code title="Проверка состояния Installer" %}

```bash
cd /home/installer
sudo ./iex-installer status
```

{% endcode %}

Если последний запуск не завершён или имеет статус ошибки, выполните:

{% code title="Продолжение установки" %}

```bash
cd /home/installer
sudo ./iex-installer \
  --config /home/installer/install.yaml \
  --non-interactive \
  --yes \
  resume
```

{% endcode %}

После завершения запустите проверку:

{% code title="Проверка установленной системы" %}

```bash
sudo /home/installer/iex-installer \
  --config /home/installer/install.yaml \
  verify
```

{% endcode %}

Используйте ту же сборку Installer и тот же файл `install.yaml`, с которыми была начата установка. Не распаковывайте поверх каталога `/home/installer` другую версию Installer.
{% endtab %}

{% tab title="Продукт уже установлен" %}
Если установка имеет успешный статус, сайт и административная панель работают, но планировщик отсутствует или выдаёт ошибку, сначала выполните диагностику:

{% code title="Диагностика планировщика" %}

```bash
sudo /home/installer/iex-installer \
  --config /home/installer/install.yaml \
  verify

systemctl is-enabled cron.service
systemctl is-active cron.service

cat /etc/cron.d/iexexchanger-scheduler

systemctl show iex-scheduler.service \
  --property=LoadState,ActiveState,Result,ExecMainStatus,User,Group,SupplementaryGroups
```

{% endcode %}

Если полная проверка показывает другие неисправности сервера, сначала устраните их. Ручная настройка планировщика не исправляет повреждённый релиз, базу данных, PHP-FPM, Redis или PostgreSQL.
{% endtab %}
{% endtabs %}

{% hint style="danger" %}
Не создавайте дополнительную задачу в пользовательском или root-crontab. Не запускайте Scheduler через `schedule:work`, Supervisor, PM2 или отдельный systemd timer. На сервере должна остаться только одна периодическая команда из `/etc/cron.d/iexexchanger-scheduler`.
{% endhint %}

## Готовый промпт для ИИ-агента

Для восстановления планировщика агенту не нужны домены, архивы продукта или ключ лицензии. Достаточно SSH-доступа к серверу. Все остальные значения должны быть получены из защищённого состояния установленной системы.

{% hint style="warning" %}
Передавайте пароль или SSH-ключ только через защищённый способ доступа. Не вставляйте действующие реквизиты в публичную документацию, отчёт или журнал агента.
{% endhint %}

{% prompt description="Промпт для технического агента" icon="microchip" defaultExpanded="partial" %}

```markdown
Ты — технический агент по восстановлению Планировщика задач iEXExchanger.

Цель: безопасно восстановить штатный Laravel Scheduler на сервере и доказать, что он запускается каждую минуту от пользователя приложения.

Доступ:
Сервер: <IP-адрес или имя сервера>
Пользователь SSH: root
Аутентификация: <SSH-ключ или пароль, переданный через защищённый канал>

Работай самостоятельно. Не запрашивай домены, ключ лицензии, архивы продукта, пользователя приложения, путь Backend или версию PHP, если их можно получить с сервера.

Сначала выполни только диагностику:

1. Проверь, что это текущая установка iEXExchanger на чистом сервере, а не FASTPANEL.
2. Проверь /etc/iexexchanger/installation.json, current release, iex-horizon.service, cron.service, iex-scheduler.service и /etc/cron.d/iexexchanger-scheduler.
3. Выполни /home/installer/iex-installer status, если исходный Installer сохранён.
4. Выполни /usr/local/sbin/iex-updater status.
5. Проверь maintenance, незавершённую установку, обновление, откат и восстановление.
6. Найди все CRON-записи и systemd timers, содержащие schedule:run.
7. Не выводи секреты, содержимое .env, пароли, ключи и токены.

Если первоначальная установка не завершилась:

- используй ту же сборку /home/installer/iex-installer;
- используй тот же /home/installer/install.yaml;
- продолжи установку командой resume;
- не запускай новый install поверх существующего состояния;
- не заменяй Installer другой сборкой.

Если установка завершилась и неисправен только планировщик:

- создай root-only резервную копию существующих unit, timer и CRON-файлов;
- установи пакет cron, только если он отсутствует;
- получи application user, public group, current/shared paths и PHP minor из защищённого installation state;
- сравни их с действующим iex-horizon.service;
- восстанови каталоги через /etc/tmpfiles.d/iexexchanger.conf;
- создай /etc/systemd/system/iex-scheduler.service как Type=oneshot;
- запускай schedule:run --no-interaction от application user;
- сохрани hardening и разреши запись только в управляемые shared-каталоги и /var/spool/iexexchanger/updater/uploads;
- создай единственный /etc/cron.d/iexexchanger-scheduler;
- установи расписание * * * * *;
- добавь проверку наличия current/backend/artisan;
- добавь проверку отсутствия current/backend/storage/framework/down;
- используй versioned PHP binary из installation state;
- отключи и убери устаревший iex-scheduler.timer;
- не удаляй посторонние CRON-задачи без точного подтверждения их назначения.

Запрещено:

- запускать Scheduler от root;
- использовать schedule:work;
- создавать второй CRON, пользовательский crontab или root-crontab;
- запускать Scheduler через Supervisor, PM2 или дополнительный timer;
- использовать chmod 777;
- выполнять рекурсивный chown для /var/www/iexexchanger;
- изменять .env, базы данных, лицензию, релизы, Nginx или PHP-FPM;
- обновлять продукт;
- удалять maintenance marker вручную;
- перезагружать сервер без доказанной необходимости.

Если обнаружен FASTPANEL, отсутствует installation state, повреждён current release, выполняется другая операция или требуются изменения за пределами планировщика, остановись и сообщи точную причину. Не создавай предполагаемую конфигурацию.

После исправления обязательно проверь:

- cron.service = enabled;
- cron.service = active;
- в CRON существует ровно один schedule:run;
- iex-scheduler.timer отсутствует;
- iex-scheduler.service имеет LoadState=loaded;
- после запуска ActiveState=inactive;
- Result=success;
- ExecMainStatus=0;
- User и Group совпадают с application user;
- SupplementaryGroups совпадает с public group;
- artisan schedule:list выполняется от application user;
- Installer verify проходит, если исходный Installer доступен.

В финальном отчёте укажи:

- найденную причину;
- выбранный способ восстановления;
- созданные или восстановленные файлы;
- путь к резервной копии;
- состояние cron.service;
- результат iex-scheduler.service;
- количество найденных CRON-записей;
- результат schedule:list;
- результат Installer verify;
- оставшиеся проблемы.

Не включай в отчёт SSH-пароль, приватный ключ, .env, токены, ключи лицензии и другие секреты.
```

{% endprompt %}

***

## Подготовка к ручной настройке

Ручная настройка допускается, если первоначальная установка уже завершена, активный релиз существует, а неисправность относится только к планировщику.

Для выполнения инструкции необходим SSH-доступ с правами `root`.

Не продолжайте настройку во время установки, обновления или отката. Во время таких операций планировщик приостанавливается специально, чтобы не запускать новые задачи одновременно с изменением приложения.

Не удаляйте вручную файлы maintenance. Сначала проверьте состояние Installer или Updater и завершите соответствующую операцию.

## Ручная настройка через терминал

Выполняйте все команды последовательно в одной SSH-сессии. Переменные, определённые на первых этапах, используются в последующих командах.

{% content-ref url="/spaces/uyjsNtEAtO6Sby8CHWyD/pages/cCEoFDjTDIufy3NUEakd" %}
[Подключение к серверу по SSH](/help-center/upravlenie-serverom/podklyuchenie-k-serveru-po-ssh.md)
{% endcontent-ref %}

{% stepper %}
{% step %}

### Подключитесь к серверу

Подключитесь по SSH:

{% code title="Подключение к серверу" %}

```bash
ssh root@IP_АДРЕС_СЕРВЕРА
```

{% endcode %}

Проверьте текущего пользователя:

{% code title="Проверка прав" %}

```bash
if [ "$(id -u)" -ne 0 ]; then
  echo "Для настройки планировщика необходим пользователь root."
  exit 1
fi
```

{% endcode %}
{% endstep %}

{% step %}

### Проверьте состояние установленной системы

Убедитесь, что существует защищённое состояние установки и работает основной фоновый процесс приложения:

{% code title="Проверка установленной системы" %}

```bash
STATE_FILE="/etc/iexexchanger/installation.json"

if [ ! -f "$STATE_FILE" ] || [ -L "$STATE_FILE" ]; then
  echo "Installation state отсутствует или имеет небезопасный тип."
  echo "Используйте resume исходного Installer."
  exit 1
fi

if [ "$(stat -c '%u:%g:%a:%h' "$STATE_FILE")" != "0:0:600:1" ]; then
  echo "Installation state должен принадлежать root и иметь режим 0600."
  exit 1
fi

systemctl is-active iex-horizon.service

if [ ! -x /usr/local/sbin/iex-updater ]; then
  echo "Updater не установлен. Первоначальная установка не завершена."
  exit 1
fi

/usr/local/sbin/iex-updater status
```

{% endcode %}

В результате `iex-horizon.service` должен иметь состояние `active`. В выводе Updater параметр `Maintenance` должен быть выключен, а последний запуск обновления или отката не должен находиться в процессе выполнения.

Если отображается активный maintenance или незавершённая операция, остановитесь и завершите её штатной командой.
{% endstep %}

{% step %}

### Установите системную службу CRON

На минимальной установке операционной системы пакет `cron` может отсутствовать. Следующая команда проверяет его наличие и устанавливает только при необходимости:

{% code title="Установка CRON" %}

```bash
if ! dpkg-query -W -f='${Status}\n' cron 2>/dev/null \
  | grep -qx 'install ok installed'; then
  apt-get update
  DEBIAN_FRONTEND=noninteractive \
    apt-get install -y --no-install-recommends cron
fi

systemctl enable --now cron.service
```

{% endcode %}

Проверьте службу:

{% code title="Проверка службы CRON" %}

```bash
systemctl is-enabled cron.service
systemctl is-active cron.service
```

{% endcode %}

Команды должны вывести `enabled` и `active`.
{% endstep %}

{% step %}

### Получите параметры текущей установки

Не указывайте пользователя, PHP и каталоги вручную. Следующие команды получают значения из защищённого состояния установки и действующей службы Horizon:

{% code title="Определение параметров установки" %}

```bash
PHP_BIN="$(
  systemctl cat iex-horizon.service \
    | sed -n 's/^ExecStart=\([^[:space:]]*\).*/\1/p' \
    | head -n 1
)"

if [ -z "$PHP_BIN" ] || [ ! -x "$PHP_BIN" ]; then
  echo "Не удалось определить рабочий PHP из iex-horizon.service."
  exit 1
fi

APP_USER="$(
  "$PHP_BIN" -r '
    $state = json_decode(
        file_get_contents($argv[1]),
        true,
        512,
        JSON_THROW_ON_ERROR
    );
    echo $state["users"]["application"];
  ' "$STATE_FILE"
)"

PUBLIC_GROUP="$(
  "$PHP_BIN" -r '
    $state = json_decode(
        file_get_contents($argv[1]),
        true,
        512,
        JSON_THROW_ON_ERROR
    );
    echo $state["users"]["publicGroup"];
  ' "$STATE_FILE"
)"

CURRENT_ROOT="$(
  "$PHP_BIN" -r '
    $state = json_decode(
        file_get_contents($argv[1]),
        true,
        512,
        JSON_THROW_ON_ERROR
    );
    echo $state["paths"]["current"];
  ' "$STATE_FILE"
)"

SHARED_DIR="$(
  "$PHP_BIN" -r '
    $state = json_decode(
        file_get_contents($argv[1]),
        true,
        512,
        JSON_THROW_ON_ERROR
    );
    echo $state["paths"]["shared"];
  ' "$STATE_FILE"
)"

PHP_MINOR="$(
  "$PHP_BIN" -r '
    $state = json_decode(
        file_get_contents($argv[1]),
        true,
        512,
        JSON_THROW_ON_ERROR
    );
    echo $state["runtime"]["phpMinor"];
  ' "$STATE_FILE"
)"

APP_GROUP="$APP_USER"
BACKEND_DIR="${CURRENT_ROOT}/backend"
ARTISAN="${BACKEND_DIR}/artisan"
```

{% endcode %}

Проверьте полученные значения перед созданием системных файлов:

{% code title="Проверка параметров" %}

```bash
printf '%s\n' "$APP_USER" \
  | grep -Eq '^[a-z_][a-z0-9_-]{0,30}$' \
  || { echo "Некорректный application user."; exit 1; }

if [ "$APP_USER" = "root" ]; then
  echo "Запуск Scheduler от root запрещён."
  exit 1
fi

if [ "$PUBLIC_GROUP" != "${APP_USER}-web" ]; then
  echo "Public group не соответствует installation state."
  exit 1
fi

printf '%s\n' "$PHP_MINOR" \
  | grep -Eq '^[0-9]+\.[0-9]+$' \
  || { echo "Некорректная версия PHP."; exit 1; }

if [ "$PHP_BIN" != "/usr/bin/php${PHP_MINOR}" ]; then
  echo "PHP в Horizon не совпадает с installation state."
  exit 1
fi

for SAFE_PATH in "$CURRENT_ROOT" "$SHARED_DIR" "$PHP_BIN"; do
  printf '%s\n' "$SAFE_PATH" \
    | grep -Eq '^/[A-Za-z0-9._/-]+$' \
    || { echo "Обнаружен небезопасный системный путь."; exit 1; }
done

id "$APP_USER"
getent group "$PUBLIC_GROUP"

test "$(systemctl show iex-horizon.service --property=User --value)" = "$APP_USER"
test "$(systemctl show iex-horizon.service --property=Group --value)" = "$APP_GROUP"
test "$(systemctl show iex-horizon.service --property=SupplementaryGroups --value)" = "$PUBLIC_GROUP"

test -L "$CURRENT_ROOT"
test -f "$ARTISAN"

if [ -e /run/iexexchanger/maintenance ] \
  || [ -e "${BACKEND_DIR}/storage/framework/down" ]; then
  echo "Приложение находится в maintenance."
  echo "Не удаляйте marker вручную. Завершите текущую операцию."
  exit 1
fi

printf 'Пользователь приложения: %s\n' "$APP_USER"
printf 'Публичная группа: %s\n' "$PUBLIC_GROUP"
printf 'PHP: %s\n' "$PHP_BIN"
printf 'Backend: %s\n' "$BACKEND_DIR"
```

{% endcode %}
{% endstep %}

{% step %}

### Восстановите управляемые каталоги

Планировщику разрешена запись только в определённые runtime-каталоги. Восстановите их режимы и владельцев из штатной конфигурации:

{% code title="Восстановление runtime-каталогов" %}

```bash
if [ ! -f /etc/tmpfiles.d/iexexchanger.conf ]; then
  echo "Управляемая конфигурация tmpfiles отсутствует."
  echo "Используйте resume исходного Installer."
  exit 1
fi

systemd-tmpfiles --create /etc/tmpfiles.d/iexexchanger.conf

for REQUIRED_DIR in \
  "${SHARED_DIR}/backend/bootstrap-cache" \
  "${SHARED_DIR}/backend/storage" \
  "${SHARED_DIR}/backend/public/images" \
  "${SHARED_DIR}/backend/public/storage" \
  "${SHARED_DIR}/backend/plugins/installed" \
  "/var/spool/iexexchanger/updater/uploads"; do
  if [ ! -d "$REQUIRED_DIR" ]; then
    echo "Отсутствует обязательный каталог: $REQUIRED_DIR"
    exit 1
  fi
done
```

{% endcode %}

Не используйте `chmod 777` и не меняйте владельца каталога `releases`. Код активного релиза должен оставаться защищённым от записи приложением.
{% endstep %}

{% step %}

### Создайте резервную копию текущей настройки

Перед заменой файлов сохраните существующую конфигурацию и crontab:

{% code title="Резервная копия планировщика" %}

```bash
for MANAGED_FILE in \
  /etc/systemd/system/iex-scheduler.service \
  /etc/cron.d/iexexchanger-scheduler; do
  if [ -L "$MANAGED_FILE" ]; then
    echo "Обнаружена небезопасная символическая ссылка: $MANAGED_FILE"
    exit 1
  fi

  if [ -e "$MANAGED_FILE" ] && [ ! -f "$MANAGED_FILE" ]; then
    echo "Обнаружен объект неподдерживаемого типа: $MANAGED_FILE"
    exit 1
  fi
done

BACKUP_DIR="$(mktemp -d /root/iex-scheduler-backup.XXXXXX)"
chmod 0700 "$BACKUP_DIR"

for ITEM in \
  /etc/systemd/system/iex-scheduler.service \
  /etc/systemd/system/iex-scheduler.timer \
  /etc/cron.d/iexexchanger-scheduler; do
  if [ -e "$ITEM" ] || [ -L "$ITEM" ]; then
    cp -a -- "$ITEM" "$BACKUP_DIR/"
  fi
done

crontab -l -u root \
  > "$BACKUP_DIR/root.crontab" 2>/dev/null || true

crontab -l -u "$APP_USER" \
  > "$BACKUP_DIR/${APP_USER}.crontab" 2>/dev/null || true

printf 'Резервная копия: %s\n' "$BACKUP_DIR"
```

{% endcode %}

Сохраните выведенный путь до завершения всех проверок.
{% endstep %}

{% step %}

### Исключите двойной запуск

Найдите дополнительные записи `schedule:run`:

{% code title="Поиск дублирующих задач" %}

```bash
DUPLICATE_CRON="$(
  grep -Rns -- 'schedule:run' \
    /etc/crontab \
    /etc/cron.d \
    /var/spool/cron/crontabs 2>/dev/null \
  | grep -v '^/etc/cron.d/iexexchanger-scheduler:' \
  || true
)"

if [ -n "$DUPLICATE_CRON" ]; then
  echo "Найдены дополнительные запуски Scheduler:"
  printf '%s\n' "$DUPLICATE_CRON"
  echo "Удалите только подтверждённые старые записи и повторите проверку."
  exit 1
fi
```

{% endcode %}

Если обнаружена старая запись в crontab пользователя или `root`, удалите только строку iEXExchanger с `schedule:run`. Не удаляйте другие задания сервера.

Отключите устаревший systemd timer:

{% code title="Отключение устаревшего timer" %}

```bash
if systemctl list-unit-files \
  --type=timer \
  --all \
  --no-legend \
  | awk '{print $1}' \
  | grep -qx 'iex-scheduler.timer'; then
  systemctl disable --now iex-scheduler.timer
fi

if [ -e /etc/systemd/system/iex-scheduler.timer ] \
  || [ -L /etc/systemd/system/iex-scheduler.timer ]; then
  mv -- \
    /etc/systemd/system/iex-scheduler.timer \
    "$BACKUP_DIR/iex-scheduler.timer.disabled"
fi
```

{% endcode %}
{% endstep %}

{% step %}

### Создайте проверочный systemd-сервис

Создайте `iex-scheduler.service` с пользователем и путями текущей установки:

{% code title="/etc/systemd/system/iex-scheduler.service" %}

```bash
UNIT_TMP="$(
  mktemp /etc/systemd/system/.iex-scheduler.service.XXXXXX
)"

cat > "$UNIT_TMP" <<EOF
[Unit]
Description=iEXExchanger Laravel Scheduler
After=network-online.target postgresql.service redis-server.service
Wants=network-online.target postgresql.service redis-server.service

[Service]
Type=oneshot
User=${APP_USER}
Group=${APP_GROUP}
SupplementaryGroups=${PUBLIC_GROUP}
WorkingDirectory=${BACKEND_DIR}
ExecStart=${PHP_BIN} ${ARTISAN} schedule:run --no-interaction
TimeoutStartSec=15min
TimeoutStopSec=90s
KillSignal=SIGTERM
UMask=0027
LimitNOFILE=10000
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
ReadWritePaths=${SHARED_DIR}/backend/bootstrap-cache
ReadWritePaths=${SHARED_DIR}/backend/storage
ReadWritePaths=${SHARED_DIR}/backend/public/images
ReadWritePaths=${SHARED_DIR}/backend/public/storage
ReadWritePaths=${SHARED_DIR}/backend/plugins/installed
ReadWritePaths=/var/spool/iexexchanger/updater/uploads
StandardOutput=journal
StandardError=journal
SyslogIdentifier=iex-scheduler
EOF

chown root:root "$UNIT_TMP"
chmod 0644 "$UNIT_TMP"

mv -f -- \
  "$UNIT_TMP" \
  /etc/systemd/system/iex-scheduler.service
```

{% endcode %}

Этот сервис предназначен для проверки одного запуска. В нём специально отсутствуют `Restart` и секция `[Install]`.
{% endstep %}

{% step %}

### Создайте расписание CRON

Создайте единственный управляемый файл планировщика:

{% code title="/etc/cron.d/iexexchanger-scheduler" %}

```bash
CRON_TMP="$(
  mktemp /etc/cron.d/.iexexchanger-scheduler.XXXXXX
)"

cat > "$CRON_TMP" <<EOF
# Управляется iEXExchanger Installer. Ручные изменения будут перезаписаны.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Laravel Scheduler запускается каждую минуту от единого пользователя сайта.
# До публикации current и во время maintenance команда безопасно пропускается.
* * * * * ${APP_USER} test -f ${ARTISAN} && test ! -e ${BACKEND_DIR}/storage/framework/down && cd ${BACKEND_DIR} && ${PHP_BIN} ${ARTISAN} schedule:run --no-interaction >/dev/null 2>&1
EOF

chown root:root "$CRON_TMP"
chmod 0644 "$CRON_TMP"

mv -f -- \
  "$CRON_TMP" \
  /etc/cron.d/iexexchanger-scheduler
```

{% endcode %}

Команда сначала проверяет наличие активного Backend и отсутствие maintenance. Во время обновления новые задачи не запускаются, а после снятия maintenance расписание продолжает работать автоматически.
{% endstep %}

{% step %}

### Примените настройку

Проверьте systemd-файл, перечитайте конфигурацию и выполните первый запуск:

{% code title="Применение настройки" %}

```bash
systemd-analyze verify \
  /etc/systemd/system/iex-scheduler.service

systemctl daemon-reload
systemctl reset-failed iex-scheduler.service

systemctl enable --now cron.service
systemctl start iex-scheduler.service
```

{% endcode %}

{% hint style="info" %}
Не выполняйте `systemctl enable iex-scheduler.service`. Постоянное расписание обеспечивает `cron.service`, а `iex-scheduler.service` запускается только для синхронной проверки.
{% endhint %}
{% endstep %}
{% endstepper %}

***

## Проверка планировщика

### Проверьте файлы и расписание

Сформируйте ожидаемую строку и сравните её с созданным файлом:

{% code title="Проверка CRON-файла" %}

```bash
EXPECTED_CRON_LINE="* * * * * ${APP_USER} test -f ${ARTISAN} && test ! -e ${BACKEND_DIR}/storage/framework/down && cd ${BACKEND_DIR} && ${PHP_BIN} ${ARTISAN} schedule:run --no-interaction >/dev/null 2>&1"

test "$(
  stat -c '%u:%g:%a:%h' \
    /etc/cron.d/iexexchanger-scheduler
)" = "0:0:644:1"

grep -Fqx -- \
  "$EXPECTED_CRON_LINE" \
  /etc/cron.d/iexexchanger-scheduler

test "$(
  grep -Fc -- \
    'schedule:run --no-interaction' \
    /etc/cron.d/iexexchanger-scheduler
)" -eq 1
```

{% endcode %}

Проверьте, что других периодических запусков нет:

{% code title="Проверка отсутствия дубликатов" %}

```bash
grep -Rns -- 'schedule:run' \
  /etc/crontab \
  /etc/cron.d \
  /var/spool/cron/crontabs 2>/dev/null
```

{% endcode %}

В результате должна отображаться только строка из `/etc/cron.d/iexexchanger-scheduler`.

### Проверьте системные службы

{% code title="Проверка CRON и Scheduler" %}

```bash
systemctl is-enabled cron.service
systemctl is-active cron.service

systemctl start iex-scheduler.service

systemctl show iex-scheduler.service \
  --property=LoadState,ActiveState,Result,ExecMainStatus,User,Group,SupplementaryGroups

test ! -e /etc/systemd/system/iex-scheduler.timer
```

{% endcode %}

Правильный результат:

```
cron.service: enabled
cron.service: active

LoadState=loaded
ActiveState=inactive
Result=success
ExecMainStatus=0
```

В свойствах `User` и `Group` должен отображаться пользователь приложения, а в `SupplementaryGroups` — его группа с окончанием `-web`.

### Проверьте зарегистрированные задачи

Получите список задач, которые зарегистрированы в текущем Backend:

{% code title="Проверка расписания Laravel" %}

```bash
/usr/sbin/runuser \
  --user "$APP_USER" \
  -- \
  "$PHP_BIN" \
  "$ARTISAN" \
  schedule:list \
  --no-interaction
```

{% endcode %}

Команда должна завершиться без PHP-ошибок и показать зарегистрированные задачи с их расписанием.

### Запустите полную проверку Installer

Если исходный каталог Installer сохранён, завершите настройку полной проверкой:

{% code title="Финальная проверка Installer" %}

```bash
if [ -x /home/installer/iex-installer ] \
  && [ -f /home/installer/install.yaml ]; then
  /home/installer/iex-installer \
    --config /home/installer/install.yaml \
    verify
fi
```

{% endcode %}

{% hint style="success" %}
Планировщик настроен правильно, если `cron.service` имеет состояния `enabled` и `active`, существует только одна CRON-команда, устаревший `iex-scheduler.timer` отсутствует, а `iex-scheduler.service` завершается с `Result=success` и `ExecMainStatus=0`.
{% endhint %}

После успешной полной проверки сохраните резервную копию из `$BACKUP_DIR` до ближайшей подтверждённой проверки работы сайта и административной панели. Затем её можно удалить вручную.

***

## Решение проблем

### `iex-horizon.service` не найден

Основные системные службы приложения не были установлены. Не создавайте только планировщик поверх незавершённой установки.

Проверьте Installer и продолжите исходную операцию:

{% code title="Продолжение незавершённой установки" %}

```bash
cd /home/installer

./iex-installer status

./iex-installer \
  --config /home/installer/install.yaml \
  --non-interactive \
  --yes \
  resume
```

{% endcode %}

### Отображается `Maintenance: ВКЛЮЧЁН`

Планировщик приостановлен из-за установки, обновления, отката или восстановления. Не удаляйте `/run/iexexchanger/maintenance` и `storage/framework/down` вручную.

Проверьте состояние операций:

{% code title="Проверка операций" %}

```bash
/home/installer/iex-installer status
/usr/local/sbin/iex-updater status
```

{% endcode %}

Сначала завершите или восстановите соответствующую операцию.

### Scheduler завершается с `Result=exit-code`

Посмотрите журнал последнего запуска:

{% code title="Журнал Scheduler" %}

```bash
journalctl \
  -u iex-scheduler.service \
  -n 100 \
  --no-pager
```

{% endcode %}

Повторите команду от пользователя приложения, а не от `root`:

{% code title="Диагностический запуск" %}

```bash
/usr/sbin/runuser \
  --user "$APP_USER" \
  -- \
  "$PHP_BIN" \
  "$ARTISAN" \
  schedule:run \
  --no-interaction
```

{% endcode %}

Исправляйте конкретную ошибку, показанную командой. Не скрывайте её перенаправлением в `/dev/null`.

### Не найден файл `artisan`

Проверьте активный релиз:

{% code title="Проверка current" %}

```bash
ls -ld "$CURRENT_ROOT"
readlink "$CURRENT_ROOT"
test -f "$ARTISAN"
```

{% endcode %}

Если `current` отсутствует, повреждён или не указывает на релиз, не создавайте другой путь вручную. Используйте `resume` Installer либо штатное восстановление незавершённого обновления.

### Отображается `Permission denied`

Восстановите управляемые каталоги:

{% code title="Восстановление владельцев и режимов" %}

```bash
systemd-tmpfiles --create \
  /etc/tmpfiles.d/iexexchanger.conf

namei -l "$ARTISAN"

systemctl start iex-scheduler.service
```

{% endcode %}

Не применяйте `chmod 777` и не выполняйте рекурсивный `chown` для `/var/www/iexexchanger`. Такие команды нарушают разграничение между защищённым кодом релиза и изменяемыми данными.

### `cron.service` не запускается

Переустановите только пакет CRON и повторите запуск:

{% code title="Восстановление CRON" %}

```bash
apt-get update

DEBIAN_FRONTEND=noninteractive \
  apt-get install --reinstall -y cron

systemctl enable --now cron.service

systemctl status cron.service \
  --no-pager \
  --full
```

{% endcode %}

Последние сообщения службы:

{% code title="Журнал CRON" %}

```bash
journalctl \
  -u cron.service \
  -n 100 \
  --no-pager
```

{% endcode %}

### Планировщик исправен, но отдельная функция не выполняется

Успешный запуск Scheduler подтверждает только выполнение зарегистрированного расписания. Он не включает автоматически настройки обменного пункта, источники курсов, мерчанты, автовыплаты или внешние интеграции.

Сначала выполните:

{% code title="Проверка задач и журналов" %}

```bash
/usr/sbin/runuser \
  --user "$APP_USER" \
  -- \
  "$PHP_BIN" \
  "$ARTISAN" \
  schedule:list \
  --no-interaction

journalctl \
  -u iex-scheduler.service \
  -n 100 \
  --no-pager

ls -la "${BACKEND_DIR}/storage/logs"
```

{% endcode %}

Если Scheduler завершён успешно, проверяйте настройки и журналы конкретной функции отдельно.
