diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b05ba9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,66 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +*.tsbuildinfo + +# Testing +coverage/ +.nyc_output/ +tests/ + +# Environment and config +.env +.env.* +*.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Git +.git/ +.gitignore +.gitattributes + +# Documentation (not needed in image) +docs/ +*.md +!README.md + +# CI/CD +.github/ +.gitlab-ci.yml + +# Session and runtime data (mounted as volumes) +sessions/ +reports/ +browser/ + +# Development files +*.log +.eslintcache +setup/ + +# Docker files (no recursion) +Dockerfile +docker-compose.yml +compose.yaml +.dockerignore + +# NixOS specific files (not needed in Docker) +flake.nix +flake.lock +run.sh + +# Asset files (not needed for runtime) +assets/ +public/ diff --git a/Dockerfile b/Dockerfile index e42e6ec..8f50a04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ############################################################################### FROM node:22-slim AS builder -WORKDIR /app +WORKDIR /usr/src/microsoft-rewards-script ENV PLAYWRIGHT_BROWSERS_PATH=0 @@ -31,7 +31,7 @@ RUN npx playwright install --with-deps --only-shell chromium \ ############################################################################### FROM node:22-slim AS runtime -WORKDIR /app +WORKDIR /usr/src/microsoft-rewards-script # Set production environment variables ENV NODE_ENV=production \ @@ -41,6 +41,9 @@ ENV NODE_ENV=production \ # Install minimal system libraries required for Chromium headless to run RUN apt-get update && apt-get install -y --no-install-recommends \ + cron \ + gettext-base \ + tzdata \ ca-certificates \ libglib2.0-0 \ libdbus-1-3 \ @@ -72,16 +75,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* # Copy compiled application and dependencies from builder stage -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/package*.json ./ -COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /usr/src/microsoft-rewards-script/dist ./dist +COPY --from=builder /usr/src/microsoft-rewards-script/package*.json ./ +COPY --from=builder /usr/src/microsoft-rewards-script/node_modules ./node_modules -# Copy entrypoint script -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh +# Copy runtime scripts with proper permissions from the start +COPY --chmod=755 src/run_daily.sh ./src/run_daily.sh +COPY --chmod=644 src/crontab.template /etc/cron.d/microsoft-rewards-cron.template +COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh -# Use entrypoint that supports single-run and optional cron mode -ENTRYPOINT ["docker-entrypoint.sh"] - -# Default: single execution -CMD ["node", "--enable-source-maps", "./dist/index.js"] +# Entrypoint handles TZ, initial run toggle, cron templating & launch +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["sh", "-c", "echo 'Container started; cron is running.'"] \ No newline at end of file diff --git a/README.md b/README.md index 073a634..64c782d 100644 --- a/README.md +++ b/README.md @@ -180,18 +180,29 @@ The bot will automatically configure cron (Linux/Raspberry Pi) or Task Scheduler --- -## Docker Quick Start +## 🐳 Docker Quick Start -For containerized deployment: +For containerized deployment with built-in scheduling: ```bash -# Ensure accounts.jsonc exists in src/ +# Ensure accounts.jsonc and config.jsonc exist in src/ docker compose up -d # View logs -docker logs -f microsoft-rewards-bot +docker logs -f microsoft-rewards-script + +# Check status +docker compose ps ``` +Container includes: +- βœ… Built-in cron scheduling +- βœ… Automatic timezone handling +- βœ… Random execution delays (anti-detection) +- βœ… Health checks + +**⚠️ Note:** Buy Mode is not available in Docker (requires interactive terminal) + **πŸ“– [Full Docker Guide](docs/docker.md)** --- diff --git a/compose.yaml b/compose.yaml index 9191a34..dbaa595 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,29 +1,43 @@ services: - microsoft-rewards-bot: + microsoft-rewards-script: build: . - container_name: microsoft-rewards-bot + container_name: microsoft-rewards-script restart: unless-stopped # Volume mounts: Specify a location where you want to save the files on your local machine. volumes: - - ./src/accounts.jsonc:/app/src/accounts.jsonc:ro - - ./src/config.jsonc:/app/src/config.jsonc:ro - - ./sessions:/app/sessions + - ./src/accounts.jsonc:/usr/src/microsoft-rewards-script/dist/accounts.jsonc:ro + - ./src/config.jsonc:/usr/src/microsoft-rewards-script/dist/config.jsonc:ro + - ./sessions:/usr/src/microsoft-rewards-script/sessions # Optional, saves your login session + - ./reports:/usr/src/microsoft-rewards-script/reports # Optional, saves run reports environment: - TZ: "America/Toronto" # Set your timezone for logging (and cron if enabled) + TZ: "America/Toronto" # Set your timezone for proper scheduling NODE_ENV: "production" - # Force headless when running in Docker (uses Chromium Headless Shell only) - FORCE_HEADLESS: "1" - - # Optional: enable in-container cron scheduling - #USE_CRON: "true" - #CRON_SCHEDULE: "0 9 * * *" # Daily at 9 AM (see https://crontab.guru) - #RUN_ON_START: "true" # Run once immediately on container start + CRON_SCHEDULE: "0 7,16,20 * * *" # Customize your schedule, use crontab.guru for formatting + RUN_ON_START: "true" # Runs the script immediately on container startup + # Add scheduled start-time randomization (uncomment to customize or disable, default: enabled) + #MIN_SLEEP_MINUTES: "5" + #MAX_SLEEP_MINUTES: "50" + SKIP_RANDOM_SLEEP: "false" + + # Optionally set how long to wait before killing a stuck script run (prevents blocking future runs, default: 8 hours) + #STUCK_PROCESS_TIMEOUT_HOURS: "8" + + # Optional resource limits for the container + mem_limit: 4g + cpus: 2 + + # Health check - monitors if cron daemon is running to ensure scheduled jobs can execute + # Container marked unhealthy if cron process dies + healthcheck: + test: ["CMD", "sh", "-c", "pgrep cron > /dev/null || exit 1"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s + # Security hardening security_opt: - - no-new-privileges:true - - # Default: single run per container start - command: ["node", "--enable-source-maps", "./dist/index.js"] \ No newline at end of file + - no-new-privileges:true \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100644 index ec54e48..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -set -e - -# Docker entrypoint with optional cron support -# Usage: -# Default: node --enable-source-maps ./dist/index.js -# Cron mode: set USE_CRON=true - -# If USE_CRON is set, configure cron for repeated runs -if [ "$USE_CRON" = "true" ] || [ "$USE_CRON" = "1" ]; then - echo "==> Cron mode enabled" - - # Default cron schedule if not provided (daily at 9 AM with random jitter) - CRON_SCHEDULE="${CRON_SCHEDULE:-0 9 * * *}" - - echo "==> Installing cron..." - apt-get update -qq && apt-get install -y -qq cron > /dev/null 2>&1 - - # Create cron job file - echo "==> Setting up cron schedule: $CRON_SCHEDULE" - - # Build environment variables for cron - ENV_VARS=$(printenv | grep -E '^(TZ|NODE_ENV|FORCE_HEADLESS|PLAYWRIGHT_BROWSERS_PATH|ACCOUNTS_JSON|ACCOUNTS_FILE)=' | sed 's/^/export /' | tr '\n' ';') - - # Create cron job that runs the script - CRON_JOB="$CRON_SCHEDULE cd /app && $ENV_VARS node --enable-source-maps ./dist/index.js >> /var/log/cron.log 2>&1" - - echo "$CRON_JOB" > /etc/cron.d/microsoft-rewards - chmod 0644 /etc/cron.d/microsoft-rewards - - # Apply cron job - crontab /etc/cron.d/microsoft-rewards - - # Create log file - touch /var/log/cron.log - - echo "==> Cron job installed:" - echo " Schedule: $CRON_SCHEDULE" - echo " Command: node --enable-source-maps ./dist/index.js" - echo " Logs: /var/log/cron.log" - echo "" - - # Run once immediately if requested - if [ "$RUN_ON_START" = "true" ] || [ "$RUN_ON_START" = "1" ]; then - echo "==> Running initial execution (RUN_ON_START=true)..." - cd /app - node --enable-source-maps ./dist/index.js 2>&1 | tee -a /var/log/cron.log - echo "==> Initial execution completed" - echo "" - fi - - echo "==> Starting cron daemon..." - echo "==> Container ready. Cron will execute: $CRON_SCHEDULE" - echo "==> View logs: docker logs -f " - echo "" - - # Start cron in foreground and tail logs - cron && tail -f /var/log/cron.log -else - echo "==> Running single execution" - echo "==> To run on a schedule inside the container, set USE_CRON=true" - echo "" - - # Execute passed command (default: node --enable-source-maps ./dist/index.js) - exec "$@" -fi diff --git a/docs/docker.md b/docs/docker.md index a8577cb..9e2080d 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,6 +1,6 @@ # 🐳 Docker Guide -Run the bot in a containerized environment with optional in-container cron support. +Run the bot in a containerized environment with built-in cron scheduling. --- @@ -8,26 +8,30 @@ Run the bot in a containerized environment with optional in-container cron suppo 1. **Create required files** - `src/accounts.jsonc` with your credentials - - `src/config.jsonc` (defaults apply if missing) + - `src/config.jsonc` (optional, defaults apply if missing) + 2. **Start the container** ```bash docker compose up -d ``` + 3. **Watch logs** ```bash - docker logs -f microsoft-rewards-bot + docker logs -f microsoft-rewards-script ``` -The container performs a single pass. Use cron, Task Scheduler, or another orchestrator to restart it on your desired cadence. +The container runs with cron scheduling enabled by default. Configure schedule via environment variables. --- ## 🎯 What's Included -- βœ… Chromium Headless Shell (lightweight browser runtime) -- βœ… Cron-ready entrypoint (`docker-entrypoint.sh`) -- βœ… Volume mounts for persistent sessions and configs -- βœ… Forced headless mode for container stability +- βœ… **Chromium Headless Shell** β€” Lightweight browser runtime +- βœ… **Built-in Cron** β€” Automated scheduling inside container +- βœ… **Volume Mounts** β€” Persistent sessions and configs +- βœ… **Forced Headless Mode** β€” Optimized for container stability +- βœ… **Health Checks** β€” Monitors cron daemon status +- βœ… **Random Sleep** β€” Spreads execution to avoid patterns --- @@ -35,9 +39,10 @@ The container performs a single pass. Use cron, Task Scheduler, or another orche | Host Path | Container Path | Purpose | |-----------|----------------|---------| -| `./src/accounts.jsonc` | `/app/src/accounts.jsonc` | Account credentials (read-only) | -| `./src/config.jsonc` | `/app/src/config.jsonc` | Configuration (read-only) | -| `./sessions` | `/app/sessions` | Cookies, fingerprints, and job-state | +| `./src/accounts.jsonc` | `/usr/src/microsoft-rewards-script/dist/accounts.jsonc` | Account credentials (read-only) | +| `./src/config.jsonc` | `/usr/src/microsoft-rewards-script/dist/config.jsonc` | Configuration (read-only) | +| `./sessions` | `/usr/src/microsoft-rewards-script/sessions` | Cookies, fingerprints, and job-state | +| `./reports` | `/usr/src/microsoft-rewards-script/reports` | Run summaries and metrics | Edit `compose.yaml` to adjust paths or add additional mounts. @@ -45,20 +50,54 @@ Edit `compose.yaml` to adjust paths or add additional mounts. ## 🌍 Environment Variables +Configure via `compose.yaml`: + ```yaml services: - microsoft-rewards-bot: + microsoft-rewards-script: environment: - TZ: "Europe/Paris" # Container timezone (cron + logging) - NODE_ENV: "production" - FORCE_HEADLESS: "1" # Required for Chromium in Docker - #USE_CRON: "true" # Optional cron mode (see below) - #CRON_SCHEDULE: "0 9 * * *" - #RUN_ON_START: "true" + # Required + TZ: "America/Toronto" # Container timezone + CRON_SCHEDULE: "0 7,16,20 * * *" # When to run (crontab format) + + # Optional + RUN_ON_START: "true" # Run immediately on startup + NODE_ENV: "production" # Node environment + + # Randomization (spreads execution time) + MIN_SLEEP_MINUTES: "5" # Min random delay (default: 5) + MAX_SLEEP_MINUTES: "50" # Max random delay (default: 50) + SKIP_RANDOM_SLEEP: "false" # Set to "true" to disable + + # Safety + STUCK_PROCESS_TIMEOUT_HOURS: "8" # Kill stuck runs (default: 8h) ``` -- `ACCOUNTS_JSON` and `ACCOUNTS_FILE` can override account sources. -- `ACCOUNTS_JSON` expects inline JSON; `ACCOUNTS_FILE` points to a mounted path. +### Key Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `TZ` | `UTC` | Container timezone for logs and scheduling | +| `CRON_SCHEDULE` | Required | Cron expression (use [crontab.guru](https://crontab.guru)) | +| `RUN_ON_START` | `false` | Run once immediately when container starts | +| `MIN_SLEEP_MINUTES` | `5` | Minimum random delay before execution | +| `MAX_SLEEP_MINUTES` | `50` | Maximum random delay before execution | +| `SKIP_RANDOM_SLEEP` | `false` | Disable randomization (not recommended) | +| `STUCK_PROCESS_TIMEOUT_HOURS` | `8` | Timeout for stuck processes | + +--- + +## πŸ• Cron Schedule Examples + +| Schedule | Description | Cron Expression | +|----------|-------------|-----------------| +| Daily at 09:00 | Single daily run | `0 9 * * *` | +| Three times daily | 07:00, 16:00, 20:00 | `0 7,16,20 * * *` | +| Every 6 hours | Four runs per day | `0 */6 * * *` | +| Weekdays at 08:00 | Monday–Friday only | `0 8 * * 1-5` | +| Twice daily | 09:00 and 21:00 | `0 9,21 * * *` | + +**Validate expressions:** [crontab.guru](https://crontab.guru) --- @@ -68,187 +107,154 @@ services: # Start container docker compose up -d -# View logs -docker logs -f microsoft-rewards-bot +# View logs (follow) +docker logs -f microsoft-rewards-script + +# View last 100 lines +docker logs --tail 100 microsoft-rewards-script # Stop container docker compose down -# Rebuild image +# Rebuild image (after code changes) docker compose build --no-cache +docker compose up -d # Restart container docker compose restart + +# Check container status +docker compose ps ``` --- -## πŸŽ›οΈ Scheduling Options - -### Use a host scheduler (recommended) - -- Trigger `docker compose up --build` (or restart the container) with cron, systemd timers, Task Scheduler, Kubernetes CronJobs, etc. -- Ensure persistent volumes are mounted so repeated runs reuse state. -- See [External Scheduling](schedule.md) for host-level examples. - -### Enable in-container cron (optional) - -1. Set environment variables in `docker-compose.yml`: - ```yaml - services: - microsoft-rewards-bot: - environment: - USE_CRON: "true" - CRON_SCHEDULE: "0 9,16,21 * * *" # Example: 09:00, 16:00, 21:00 - RUN_ON_START: "true" # Optional one-time run at container boot - ``` -2. Rebuild and redeploy: - ```bash - docker compose down - docker compose build --no-cache - docker compose up -d - ``` -3. Confirm cron is active: - ```bash - docker logs -f microsoft-rewards-bot - ``` - -#### Cron schedule examples - -| Schedule | Description | Cron expression | -|----------|-------------|-----------------| -| Daily at 09:00 | Single run | `0 9 * * *` | -| Twice daily | 09:00 & 21:00 | `0 9,21 * * *` | -| Every 6 hours | Four runs/day | `0 */6 * * *` | -| Weekdays at 08:00 | Monday–Friday | `0 8 * * 1-5` | - -Validate expressions with [crontab.guru](https://crontab.guru). - ---- - ## πŸ› οΈ Troubleshooting | Problem | Solution | |---------|----------| -| **"accounts.json not found"** | Ensure `./src/accounts.jsonc` exists and is mounted read-only | -| **"Browser launch failed"** | Verify `FORCE_HEADLESS=1` and Chromium dependencies installed | -| **"Permission denied"** | Check file permissions (`chmod 644 accounts.jsonc config.jsonc`) | -| **Automation not repeating** | Enable cron (`USE_CRON=true`) or use a host scheduler | -| **Cron not working** | See [Cron troubleshooting](#-cron-troubleshooting) | +| **"accounts.jsonc not found"** | Ensure file exists at `./src/accounts.jsonc` | +| **"Browser launch failed"** | Verify `FORCE_HEADLESS=1` is set (automatic in Dockerfile) | +| **"Permission denied"** | Fix file permissions: `chmod 644 src/*.jsonc` | +| **Cron not running** | Check logs for "Cron configured" message | +| **Wrong timezone** | Update `TZ` in `compose.yaml` and restart | +| **No output in logs** | Wait for cron schedule or set `RUN_ON_START=true` | -### Debug container +### Debug Container ```bash # Enter container shell -docker exec -it microsoft-rewards-bot /bin/bash +docker exec -it microsoft-rewards-script /bin/bash # Check Node.js version -docker exec -it microsoft-rewards-bot node --version +docker exec -it microsoft-rewards-script node --version # Inspect mounted config -docker exec -it microsoft-rewards-bot cat /app/src/config.jsonc +docker exec -it microsoft-rewards-script cat /usr/src/microsoft-rewards-script/dist/config.jsonc -# Check env vars -docker exec -it microsoft-rewards-bot printenv | grep -E "TZ|USE_CRON|CRON_SCHEDULE" +# Check environment variables +docker exec -it microsoft-rewards-script printenv | grep -E "TZ|CRON" + +# View cron configuration +docker exec -it microsoft-rewards-script crontab -l + +# Check if cron is running +docker exec -it microsoft-rewards-script ps aux | grep cron ``` --- -## πŸ”„ Switching cron on or off +## οΏ½ Health Check -- **Enable cron:** set `USE_CRON=true`, provide `CRON_SCHEDULE`, rebuild, and redeploy. -- **Disable cron:** remove `USE_CRON` (and related variables). The container will run once per start; handle recurrence externally. +The container includes a health check that monitors the cron daemon: + +```yaml +healthcheck: + test: ["CMD", "sh", "-c", "pgrep cron > /dev/null || exit 1"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 30s +``` + +Check health status: +```bash +docker inspect --format='{{.State.Health.Status}}' microsoft-rewards-script +``` --- -## πŸ› Cron troubleshooting +## ⚠️ Important Notes -| Problem | Solution | -|---------|----------| -| **Cron not executing** | Check logs for "Cron mode enabled" and cron syntax errors | -| **Wrong timezone** | Ensure `TZ` matches your location | -| **Syntax error** | Validate expression at [crontab.guru](https://crontab.guru) | -| **No logs generated** | Tail `/var/log/cron.log` inside the container | -| **Duplicate runs** | Ensure only one cron entry is configured | +### Buy Mode Not Supported -### Inspect cron inside the container +**Buy Mode cannot be used in Docker** because it requires interactive terminal input. Use Buy Mode only in local installations: ```bash -docker exec -it microsoft-rewards-bot /bin/bash -ps aux | grep cron -crontab -l -tail -100 /var/log/cron.log +# βœ… Works locally +npm run buy + +# ❌ Does not work in Docker +docker exec microsoft-rewards-script npm run buy ``` ---- +For manual redemptions, run the bot locally outside Docker. -## πŸ“š Next steps +### Headless Mode Required -- [Configuration guide](config.md) -- [External scheduling](schedule.md) -- [Humanization guide](humanization.md) +Docker containers **must run in headless mode**. The Dockerfile automatically sets `FORCE_HEADLESS=1`. Do not disable this. + +### Random Sleep Behavior + +- **Enabled by default** to avoid detection patterns +- Adds 5-50 minutes random delay before each run +- Disable only for testing: `SKIP_RANDOM_SLEEP=true` +- First run (when `RUN_ON_START=true`) skips random sleep --- -### Option 3: Single Run (Manual) +## οΏ½ Resource Limits + +Recommended settings in `compose.yaml`: ```yaml services: - rewards: - build: . - command: ["node", "./dist/index.js"] + microsoft-rewards-script: + mem_limit: 4g # Maximum RAM + cpus: 2 # CPU cores ``` +Adjust based on your system and number of accounts. + --- -## πŸ”„ Switching Cron On or Off +## πŸ”’ Security Hardening -- **Enable cron:** set `USE_CRON=true`, provide `CRON_SCHEDULE`, rebuild the image, and redeploy. -- **Disable cron:** remove `USE_CRON` (and related variables). The container will run once per start; use host automation to relaunch when needed. +The compose file includes security measures: ---- - -## πŸ› Cron Troubleshooting - -| Problem | Solution | -|---------|----------| -| **Cron not executing** | Check `docker logs` for "Cron mode enabled" message | -| **Wrong timezone** | Verify `TZ` environment variable matches your location | -| **Syntax error** | Validate cron expression at [crontab.guru](https://crontab.guru) | -| **No logs** | Use `docker exec tail -f /var/log/cron.log` | -| **Multiple executions** | Check for duplicate cron entries | - -### Debug Cron Inside Container - -```bash -# Enter container -docker exec -it microsoft-rewards-bot /bin/bash - -# Check cron is running -ps aux | grep cron - -# View installed cron jobs -crontab -l - -# Check cron logs -tail -100 /var/log/cron.log - -# Test environment variables -printenv | grep -E 'TZ|NODE_ENV' +```yaml +security_opt: + - no-new-privileges:true # Prevents privilege escalation ``` +Volumes are mounted **read-only** (`:ro`) for credentials to prevent tampering. + --- ## πŸ“š Next Steps -**Need 2FA?** -β†’ **[Accounts & TOTP Setup](./accounts.md)** +**Need 2FA setup?** +β†’ **[Accounts & TOTP Guide](./accounts.md)** **Want notifications?** -β†’ **[Discord Webhooks](./conclusionwebhook.md)** +β†’ **[Discord Webhooks](./conclusionwebhook.md)** +β†’ **[NTFY Push Alerts](./ntfy.md)** -**Need scheduling tips?** -β†’ **[External Scheduling](./schedule.md)** +**Need proxy configuration?** +β†’ **[Proxy Setup](./proxy.md)** + +**External scheduling?** +β†’ **[Scheduling Guide](./schedule.md)** --- diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..22d53b8 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure Playwright uses preinstalled browsers +export PLAYWRIGHT_BROWSERS_PATH=0 + +# 1. Timezone: default to UTC if not provided +: "${TZ:=UTC}" +ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime +echo "$TZ" > /etc/timezone +dpkg-reconfigure -f noninteractive tzdata + +# 2. Validate CRON_SCHEDULE +if [ -z "${CRON_SCHEDULE:-}" ]; then + echo "ERROR: CRON_SCHEDULE environment variable is not set." >&2 + echo "Please set CRON_SCHEDULE (e.g., \"0 2 * * *\")." >&2 + exit 1 +fi + +# 3. Initial run without sleep if RUN_ON_START=true +if [ "${RUN_ON_START:-false}" = "true" ]; then + echo "[entrypoint] Starting initial run in background at $(date)" + ( + cd /usr/src/microsoft-rewards-script || { + echo "[entrypoint-bg] ERROR: Unable to cd to /usr/src/microsoft-rewards-script" >&2 + exit 1 + } + # Skip random sleep for initial run, but preserve setting for cron jobs + SKIP_RANDOM_SLEEP=true src/run_daily.sh + echo "[entrypoint-bg] Initial run completed at $(date)" + ) & + echo "[entrypoint] Background process started (PID: $!)" +fi + +# 4. Template and register cron file with explicit timezone export +if [ ! -f /etc/cron.d/microsoft-rewards-cron.template ]; then + echo "ERROR: Cron template /etc/cron.d/microsoft-rewards-cron.template not found." >&2 + exit 1 +fi + +# Export TZ for envsubst to use +export TZ +envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron +chmod 0644 /etc/cron.d/microsoft-rewards-cron +crontab /etc/cron.d/microsoft-rewards-cron + +echo "[entrypoint] Cron configured with schedule: $CRON_SCHEDULE and timezone: $TZ; starting cron at $(date)" + +# 5. Start cron in foreground (PID 1) +exec cron -f \ No newline at end of file diff --git a/src/config.jsonc b/src/config.jsonc index ed3f59f..d7390ec 100644 --- a/src/config.jsonc +++ b/src/config.jsonc @@ -42,7 +42,7 @@ // Search "search": { - "useLocalQueries": true, + "useLocalQueries": false, "settings": { "useGeoLocaleQueries": true, "scrollRandomResults": true, diff --git a/src/crontab.template b/src/crontab.template new file mode 100644 index 0000000..6466377 --- /dev/null +++ b/src/crontab.template @@ -0,0 +1,2 @@ +# Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs +${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /proc/1/fd/1 2>&1 \ No newline at end of file diff --git a/src/run_daily.sh b/src/run_daily.sh new file mode 100644 index 0000000..9ee9271 --- /dev/null +++ b/src/run_daily.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PATH="/usr/local/bin:/usr/bin:/bin" +export PLAYWRIGHT_BROWSERS_PATH=0 +export TZ="${TZ:-UTC}" + +cd /usr/src/microsoft-rewards-script + +LOCKFILE=/tmp/run_daily.lock + +# ------------------------------- +# Function: Check and fix lockfile integrity +# ------------------------------- +self_heal_lockfile() { + # If lockfile exists but is empty β†’ remove it + if [ -f "$LOCKFILE" ]; then + local lock_content + lock_content=$(<"$LOCKFILE" || echo "") + + if [[ -z "$lock_content" ]]; then + echo "[$(date)] [run_daily.sh] Found empty lockfile β†’ removing." + rm -f "$LOCKFILE" + return + fi + + # If lockfile contains non-numeric PID β†’ remove it + if ! [[ "$lock_content" =~ ^[0-9]+$ ]]; then + echo "[$(date)] [run_daily.sh] Found corrupted lockfile content ('$lock_content') β†’ removing." + rm -f "$LOCKFILE" + return + fi + + # If lockfile contains PID but process is dead β†’ remove it + if ! kill -0 "$lock_content" 2>/dev/null; then + echo "[$(date)] [run_daily.sh] Lockfile PID $lock_content is dead β†’ removing stale lock." + rm -f "$LOCKFILE" + return + fi + fi +} + +# ------------------------------- +# Function: Acquire lock +# ------------------------------- +acquire_lock() { + local max_attempts=5 + local attempt=0 + local timeout_hours=${STUCK_PROCESS_TIMEOUT_HOURS:-8} + local timeout_seconds=$((timeout_hours * 3600)) + + while [ $attempt -lt $max_attempts ]; do + # Try to create lock with current PID + if (set -C; echo "$$" > "$LOCKFILE") 2>/dev/null; then + echo "[$(date)] [run_daily.sh] Lock acquired successfully (PID: $$)" + return 0 + fi + + # Lock exists, validate it + if [ -f "$LOCKFILE" ]; then + local existing_pid + existing_pid=$(<"$LOCKFILE" || echo "") + + echo "[$(date)] [run_daily.sh] Lock file exists with PID: '$existing_pid'" + + # If lockfile content is invalid β†’ delete and retry + if [[ -z "$existing_pid" || ! "$existing_pid" =~ ^[0-9]+$ ]]; then + echo "[$(date)] [run_daily.sh] Removing invalid lockfile β†’ retrying..." + rm -f "$LOCKFILE" + continue + fi + + # If process is dead β†’ delete and retry + if ! kill -0 "$existing_pid" 2>/dev/null; then + echo "[$(date)] [run_daily.sh] Removing stale lock (dead PID: $existing_pid)" + rm -f "$LOCKFILE" + continue + fi + + # Check process runtime β†’ kill if exceeded timeout + local process_age + if process_age=$(ps -o etimes= -p "$existing_pid" 2>/dev/null | tr -d ' '); then + if [ "$process_age" -gt "$timeout_seconds" ]; then + echo "[$(date)] [run_daily.sh] Killing stuck process $existing_pid (${process_age}s > ${timeout_hours}h)" + kill -TERM "$existing_pid" 2>/dev/null || true + sleep 5 + kill -KILL "$existing_pid" 2>/dev/null || true + rm -f "$LOCKFILE" + continue + fi + fi + fi + + echo "[$(date)] [run_daily.sh] Lock held by PID $existing_pid, attempt $((attempt + 1))/$max_attempts" + sleep 2 + ((attempt++)) + done + + echo "[$(date)] [run_daily.sh] Could not acquire lock after $max_attempts attempts; exiting." + return 1 +} + +# ------------------------------- +# Function: Release lock +# ------------------------------- +release_lock() { + if [ -f "$LOCKFILE" ]; then + local lock_pid + lock_pid=$(<"$LOCKFILE") + if [ "$lock_pid" = "$$" ]; then + rm -f "$LOCKFILE" + echo "[$(date)] [run_daily.sh] Lock released (PID: $$)" + fi + fi +} + +# Always release lock on exit β€” but only if we acquired it +trap 'release_lock' EXIT INT TERM + +# ------------------------------- +# MAIN EXECUTION FLOW +# ------------------------------- +echo "[$(date)] [run_daily.sh] Current process PID: $$" + +# Self-heal any broken or empty locks before proceeding +self_heal_lockfile + +# Attempt to acquire the lock safely +if ! acquire_lock; then + exit 0 +fi + +# Random sleep between MIN and MAX to spread execution +MINWAIT=${MIN_SLEEP_MINUTES:-5} +MAXWAIT=${MAX_SLEEP_MINUTES:-50} +MINWAIT_SEC=$((MINWAIT*60)) +MAXWAIT_SEC=$((MAXWAIT*60)) + +if [ "${SKIP_RANDOM_SLEEP:-false}" != "true" ]; then + SLEEPTIME=$(( MINWAIT_SEC + RANDOM % (MAXWAIT_SEC - MINWAIT_SEC) )) + echo "[$(date)] [run_daily.sh] Sleeping for $((SLEEPTIME/60)) minutes ($SLEEPTIME seconds)" + sleep "$SLEEPTIME" +else + echo "[$(date)] [run_daily.sh] Skipping random sleep" +fi + +# Start the actual script +echo "[$(date)] [run_daily.sh] Starting script..." +if npm start; then + echo "[$(date)] [run_daily.sh] Script completed successfully." +else + echo "[$(date)] [run_daily.sh] ERROR: Script failed!" >&2 +fi + +echo "[$(date)] [run_daily.sh] Script finished" +# Lock is released automatically via trap \ No newline at end of file