mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
New structure
This commit is contained in:
92
docker/Dockerfile
Normal file
92
docker/Dockerfile
Normal file
@@ -0,0 +1,92 @@
|
||||
###############################################################################
|
||||
# Stage 1: Builder
|
||||
###############################################################################
|
||||
FROM node:22-slim AS builder
|
||||
|
||||
WORKDIR /usr/src/microsoft-rewards-bot
|
||||
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json tsconfig.json ./
|
||||
|
||||
# Install all dependencies required to build the script
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Copy source and build
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Remove build dependencies, and reinstall only runtime dependencies
|
||||
RUN rm -rf node_modules \
|
||||
&& npm ci --omit=dev --ignore-scripts \
|
||||
&& npm cache clean --force
|
||||
|
||||
# Install Chromium Headless Shell, and cleanup
|
||||
RUN npx playwright install --with-deps --only-shell chromium \
|
||||
&& rm -rf /root/.cache /tmp/* /var/tmp/*
|
||||
|
||||
###############################################################################
|
||||
# Stage 2: Runtime
|
||||
###############################################################################
|
||||
FROM node:22-slim AS runtime
|
||||
|
||||
WORKDIR /usr/src/microsoft-rewards-bot
|
||||
|
||||
# Set production environment variables
|
||||
ENV NODE_ENV=production \
|
||||
TZ=UTC \
|
||||
PLAYWRIGHT_BROWSERS_PATH=0 \
|
||||
FORCE_HEADLESS=1
|
||||
|
||||
# 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 \
|
||||
libexpat1 \
|
||||
libfontconfig1 \
|
||||
libgtk-3-0 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libasound2 \
|
||||
libflac12 \
|
||||
libatk1.0-0 \
|
||||
libatspi2.0-0 \
|
||||
libdrm2 \
|
||||
libgbm1 \
|
||||
libdav1d6 \
|
||||
libx11-6 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
libdouble-conversion3 \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
||||
|
||||
# Copy compiled application and dependencies from builder stage
|
||||
COPY --from=builder /usr/src/microsoft-rewards-bot/dist ./dist
|
||||
COPY --from=builder /usr/src/microsoft-rewards-bot/package*.json ./
|
||||
COPY --from=builder /usr/src/microsoft-rewards-bot/node_modules ./node_modules
|
||||
|
||||
# Copy runtime scripts with proper permissions and normalize line endings for non-Unix users
|
||||
# IMPROVED: Scripts now organized in docker/ folder
|
||||
COPY --chmod=755 docker/run_daily.sh ./docker/run_daily.sh
|
||||
COPY --chmod=644 docker/crontab.template /etc/cron.d/microsoft-rewards-cron.template
|
||||
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh \
|
||||
&& sed -i 's/\r$//' ./docker/run_daily.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
docker/compose.yaml
Normal file
42
docker/compose.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
microsoft-rewards-script:
|
||||
build: .
|
||||
container_name: microsoft-rewards-bot
|
||||
restart: unless-stopped
|
||||
|
||||
# Volume mounts: Specify a location where you want to save the files on your local machine.
|
||||
volumes:
|
||||
- ./src/accounts.jsonc:/usr/src/microsoft-rewards-bot/dist/accounts.jsonc:ro
|
||||
- ./src/config.jsonc:/usr/src/microsoft-rewards-bot/dist/config.jsonc:ro
|
||||
- ./sessions:/usr/src/microsoft-rewards-bot/dist/browser/sessions # Optional, saves your login session
|
||||
|
||||
environment:
|
||||
TZ: "America/Toronto" # Set your timezone for proper scheduling
|
||||
NODE_ENV: "production"
|
||||
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
|
||||
3
docker/crontab.template
Normal file
3
docker/crontab.template
Normal file
@@ -0,0 +1,3 @@
|
||||
# Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs
|
||||
${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-bot/docker/run_daily.sh >> /proc/1/fd/1 2>&1
|
||||
|
||||
50
docker/entrypoint.sh
Normal file
50
docker/entrypoint.sh
Normal 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-bot || {
|
||||
echo "[entrypoint-bg] ERROR: Unable to cd to /usr/src/microsoft-rewards-bot" >&2
|
||||
exit 1
|
||||
}
|
||||
# Skip random sleep for initial run, but preserve setting for cron jobs
|
||||
SKIP_RANDOM_SLEEP=true docker/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
|
||||
156
docker/run_daily.sh
Normal file
156
docker/run_daily.sh
Normal 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-bot
|
||||
|
||||
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