mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-23 16:31:04 +00:00
Preliminary dockerization with scheduling (#406)
Porting working docker implementation (scheduling, etc.) into revised v2 Co-authored-by: Netsky <56271887+TheNetsky@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8c8bdaf3e5
commit
49b607d78c
13
Dockerfile
13
Dockerfile
@@ -41,6 +41,9 @@ ENV NODE_ENV=production \
|
|||||||
|
|
||||||
# Install minimal system libraries required for Chromium headless to run
|
# Install minimal system libraries required for Chromium headless to run
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
cron \
|
||||||
|
gettext-base \
|
||||||
|
tzdata \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libglib2.0-0 \
|
libglib2.0-0 \
|
||||||
libdbus-1-3 \
|
libdbus-1-3 \
|
||||||
@@ -76,5 +79,11 @@ 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/package*.json ./
|
||||||
COPY --from=builder /usr/src/microsoft-rewards-script/node_modules ./node_modules
|
COPY --from=builder /usr/src/microsoft-rewards-script/node_modules ./node_modules
|
||||||
|
|
||||||
# Run the scheduled rewards script
|
# Copy runtime scripts with proper permissions from the start
|
||||||
CMD ["npm", "run", "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
|
||||||
|
|
||||||
|
# Entrypoint handles TZ, initial run toggle, cron templating & launch
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
CMD ["sh", "-c", "echo 'Container started; cron is running.'"]
|
||||||
|
|||||||
42
compose.yaml
42
compose.yaml
@@ -6,25 +6,37 @@ services:
|
|||||||
|
|
||||||
# Volume mounts: Specify a location where you want to save the files on your local machine.
|
# Volume mounts: Specify a location where you want to save the files on your local machine.
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/accounts.json:/usr/src/microsoft-rewards-script/accounts.json:ro
|
- ./src/accounts.jsonc:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
||||||
- ./src/config.jsonc:/usr/src/microsoft-rewards-script/config.json:ro
|
- ./src/config.jsonc:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
||||||
- ./sessions:/usr/src/microsoft-rewards-script/sessions # Optional, saves your login session
|
- ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
TZ: "America/Toronto" # Set your timezone for proper scheduling (used by image and scheduler)
|
TZ: "America/Toronto" # Set your timezone for proper scheduling
|
||||||
NODE_ENV: "production"
|
NODE_ENV: "production"
|
||||||
# Force headless when running in Docker (uses Chromium Headless Shell only)
|
CRON_SCHEDULE: "0 7,16,20 * * *" # Customize your schedule, use crontab.guru for formatting
|
||||||
FORCE_HEADLESS: "1"
|
RUN_ON_START: "true" # Runs the script immediately on container startup
|
||||||
#SCHEDULER_DAILY_JITTER_MINUTES_MIN: "2"
|
|
||||||
#SCHEDULER_DAILY_JITTER_MINUTES_MAX: "10"
|
# Add scheduled start-time randomization (uncomment to customize or disable, default: enabled)
|
||||||
# Watchdog timeout per pass (minutes, default 180)
|
#MIN_SLEEP_MINUTES: "5"
|
||||||
#SCHEDULER_PASS_TIMEOUT_MINUTES: "180"
|
#MAX_SLEEP_MINUTES: "50"
|
||||||
# Run pass in child process (default true). Set to "false" to disable for debugging.
|
SKIP_RANDOM_SLEEP: "false"
|
||||||
#SCHEDULER_FORK_PER_PASS: "true"
|
|
||||||
|
# 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 hardening
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
# Use the built-in scheduler by default; override with `command:` for one-shot runs
|
|
||||||
command: ["npm", "run", "start"]
|
|
||||||
|
|||||||
50
entrypoint.sh
Executable file
50
entrypoint.sh
Executable file
@@ -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
|
||||||
2
src/crontab.template
Normal file
2
src/crontab.template
Normal file
@@ -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
|
||||||
156
src/run_daily.sh
Executable file
156
src/run_daily.sh
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user