mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 01:06:17 +00:00
Features: Improved handling of navigation errors and automatic updates, optimized wait times, and cron job configuration
This commit is contained in:
@@ -8,12 +8,10 @@ WORKDIR /usr/src/microsoft-rewards-script
|
|||||||
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
||||||
|
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package.json tsconfig.json ./
|
COPY package.json package-lock.json tsconfig.json ./
|
||||||
|
|
||||||
# Generate fresh lockfile for target platform architecture
|
# Install all dependencies required to build the script
|
||||||
# This ensures native dependencies are resolved correctly
|
RUN npm ci --ignore-scripts
|
||||||
RUN npm install --ignore-scripts --package-lock-only \
|
|
||||||
&& npm ci --ignore-scripts
|
|
||||||
|
|
||||||
# Copy source and build
|
# Copy source and build
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -400,8 +400,8 @@ async function main() {
|
|||||||
code = await updateDocker()
|
code = await updateDocker()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: Always exit with code so external schedulers can react correctly
|
// Return exit code to parent process
|
||||||
// Otherwise the process hangs indefinitely and gets killed by watchdog
|
// This allows the bot to know if update succeeded (0) or failed (non-zero)
|
||||||
process.exit(code)
|
process.exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,45 +105,53 @@ export class Login {
|
|||||||
const isLinux = process.platform === 'linux'
|
const isLinux = process.platform === 'linux'
|
||||||
const navigationTimeout = isLinux ? 60000 : 30000
|
const navigationTimeout = isLinux ? 60000 : 30000
|
||||||
|
|
||||||
// Try initial navigation with error handling for chrome-error interruption
|
// IMPROVEMENT: Try initial navigation with better error handling
|
||||||
let navigationSucceeded = false
|
let navigationSucceeded = false
|
||||||
let recoveryUsed = false
|
let recoveryUsed = false
|
||||||
try {
|
let attempts = 0
|
||||||
await page.goto('https://www.bing.com/rewards/dashboard', {
|
const maxAttempts = 3
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: navigationTimeout
|
while (!navigationSucceeded && attempts < maxAttempts) {
|
||||||
})
|
attempts++
|
||||||
navigationSucceeded = true
|
try {
|
||||||
} catch (error) {
|
await page.goto('https://www.bing.com/rewards/dashboard', {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: navigationTimeout
|
||||||
// If interrupted by chrome-error, retry with reload approach
|
})
|
||||||
if (errorMsg.includes('chrome-error://chromewebdata/')) {
|
navigationSucceeded = true
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Navigation interrupted by chrome-error, attempting recovery...', 'warn')
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
// Wait a bit for page to settle
|
// If interrupted by chrome-error, retry with reload approach
|
||||||
await this.bot.utils.wait(1000)
|
if (errorMsg.includes('chrome-error://chromewebdata/')) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', `Navigation interrupted by chrome-error (attempt ${attempts}/${maxAttempts}), attempting recovery...`, 'warn')
|
||||||
// Try reload which usually fixes the issue
|
|
||||||
try {
|
// Wait a bit for page to settle
|
||||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
|
await this.bot.utils.wait(1500) // Increased from 1000ms
|
||||||
navigationSucceeded = true
|
|
||||||
recoveryUsed = true
|
// Try reload which usually fixes the issue
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Recovery successful via reload')
|
try {
|
||||||
} catch (reloadError) {
|
await page.reload({ waitUntil: 'domcontentloaded', timeout: navigationTimeout })
|
||||||
// Last resort: try goto again
|
navigationSucceeded = true
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Reload failed, trying fresh navigation...', 'warn')
|
recoveryUsed = true
|
||||||
await this.bot.utils.wait(1500)
|
this.bot.log(this.bot.isMobile, 'LOGIN', '✓ Recovery successful via reload')
|
||||||
await page.goto('https://www.bing.com/rewards/dashboard', {
|
} catch (reloadError) {
|
||||||
waitUntil: 'domcontentloaded',
|
// Last resort: try goto again
|
||||||
timeout: navigationTimeout
|
if (attempts < maxAttempts) {
|
||||||
})
|
this.bot.log(this.bot.isMobile, 'LOGIN', `Reload failed (attempt ${attempts}/${maxAttempts}), trying fresh navigation...`, 'warn')
|
||||||
navigationSucceeded = true
|
await this.bot.utils.wait(2000) // Increased from 1500ms
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Recovery successful via fresh navigation')
|
} else {
|
||||||
|
throw reloadError // Exhausted attempts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (attempts < maxAttempts) {
|
||||||
|
// Different error, retry with backoff
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', `Navigation failed (attempt ${attempts}/${maxAttempts}): ${errorMsg}`, 'warn')
|
||||||
|
await this.bot.utils.wait(2000 * attempts) // Exponential backoff
|
||||||
|
} else {
|
||||||
|
// Exhausted attempts, rethrow
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Different error, rethrow
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,37 +519,55 @@ export class Login {
|
|||||||
private async inputEmail(page: Page, email: string) {
|
private async inputEmail(page: Page, email: string) {
|
||||||
// Check for passkey prompts first
|
// Check for passkey prompts first
|
||||||
await this.handlePasskeyPrompts(page, 'main')
|
await this.handlePasskeyPrompts(page, 'main')
|
||||||
await this.bot.utils.wait(250)
|
await this.bot.utils.wait(500) // Increased from 250ms
|
||||||
|
|
||||||
|
// IMPROVEMENT: Wait for page to be fully ready before looking for email field
|
||||||
|
await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {})
|
||||||
|
await this.bot.utils.wait(300) // Extra settling time
|
||||||
|
|
||||||
if (await this.tryAutoTotp(page, 'pre-email check')) {
|
if (await this.tryAutoTotp(page, 'pre-email check')) {
|
||||||
await this.bot.utils.wait(800)
|
await this.bot.utils.wait(1000) // Increased from 800ms
|
||||||
}
|
}
|
||||||
|
|
||||||
let field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null)
|
// IMPROVEMENT: More retries with better timing
|
||||||
|
let field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 8000 }).catch(()=>null) // Increased from 5000ms
|
||||||
if (!field) {
|
if (!field) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found (first attempt), retrying...', 'warn')
|
||||||
|
|
||||||
const totpHandled = await this.tryAutoTotp(page, 'pre-email challenge')
|
const totpHandled = await this.tryAutoTotp(page, 'pre-email challenge')
|
||||||
if (totpHandled) {
|
if (totpHandled) {
|
||||||
await this.bot.utils.wait(800)
|
await this.bot.utils.wait(1200) // Increased from 800ms
|
||||||
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null)
|
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 8000 }).catch(()=>null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
// Try one more time after handling possible passkey prompts
|
// Try one more time after handling possible passkey prompts
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found (second attempt), trying passkey/reload...', 'warn')
|
||||||
await this.handlePasskeyPrompts(page, 'main')
|
await this.handlePasskeyPrompts(page, 'main')
|
||||||
await this.bot.utils.wait(500)
|
await this.bot.utils.wait(800) // Increased from 500ms
|
||||||
|
|
||||||
|
// IMPROVEMENT: Try page reload if field still missing (common issue on first load)
|
||||||
|
const content = await page.content().catch(() => '')
|
||||||
|
if (content.length < 1000) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page content too small, reloading...', 'warn')
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => {})
|
||||||
|
await this.bot.utils.wait(1500)
|
||||||
|
}
|
||||||
|
|
||||||
const totpRetry = await this.tryAutoTotp(page, 'pre-email retry')
|
const totpRetry = await this.tryAutoTotp(page, 'pre-email retry')
|
||||||
if (totpRetry) {
|
if (totpRetry) {
|
||||||
await this.bot.utils.wait(800)
|
await this.bot.utils.wait(1200) // Increased from 800ms
|
||||||
}
|
}
|
||||||
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 3000 }).catch(()=>null)
|
|
||||||
|
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null)
|
||||||
if (!field && this.totpAttempts > 0) {
|
if (!field && this.totpAttempts > 0) {
|
||||||
await this.bot.utils.wait(2000)
|
await this.bot.utils.wait(2500) // Increased from 2000ms
|
||||||
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 3000 }).catch(()=>null)
|
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null) // Increased from 3000ms
|
||||||
}
|
}
|
||||||
if (!field) {
|
if (!field) {
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not present', 'warn')
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not present after all retries', 'error')
|
||||||
return
|
throw new Error('Login form email field not found after multiple attempts')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,14 @@ export class Workers {
|
|||||||
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
|
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
|
||||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
|
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
|
||||||
|
|
||||||
|
// Check if element exists before clicking (avoid 30s timeout)
|
||||||
|
try {
|
||||||
|
await page.waitForSelector(selector, { timeout: 5000 })
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Activity selector not found (might be completed or unavailable): ${selector}`, 'warn')
|
||||||
|
return // Skip this activity gracefully instead of waiting 30s
|
||||||
|
}
|
||||||
|
|
||||||
await page.click(selector)
|
await page.click(selector)
|
||||||
page = await this.bot.browser.utils.getLatestTab(page)
|
page = await this.bot.browser.utils.getLatestTab(page)
|
||||||
|
|
||||||
|
|||||||
33
src/index.ts
33
src/index.ts
@@ -497,7 +497,10 @@ export class MicrosoftRewardsBot {
|
|||||||
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.runAutoUpdate()
|
const updateCode = await this.runAutoUpdate()
|
||||||
|
if (updateCode === 0) {
|
||||||
|
log('main', 'UPDATE', '✅ Update successful - next run will use new version', 'log', 'green')
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
}
|
}
|
||||||
@@ -750,9 +753,19 @@ export class MicrosoftRewardsBot {
|
|||||||
// Single process mode -> build and send conclusion directly
|
// Single process mode -> build and send conclusion directly
|
||||||
await this.sendConclusion(this.accountSummaries)
|
await this.sendConclusion(this.accountSummaries)
|
||||||
// After conclusion, run optional auto-update
|
// After conclusion, run optional auto-update
|
||||||
await this.runAutoUpdate().catch((e) => {
|
const updateResult = await this.runAutoUpdate().catch((e) => {
|
||||||
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
return 1 // Error code
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If update was successful (code 0), restart the script to use the new version
|
||||||
|
// This is critical for cron jobs - they need to apply updates immediately
|
||||||
|
if (updateResult === 0) {
|
||||||
|
log('main', 'UPDATE', '✅ Update successful - restarting with new version...', 'log', 'green')
|
||||||
|
// On Raspberry Pi/Linux with cron, just exit - cron will handle next run
|
||||||
|
// No need to restart immediately, next scheduled run will use new code
|
||||||
|
log('main', 'UPDATE', 'Next scheduled run will use the updated code', 'log')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
process.exit()
|
process.exit()
|
||||||
}
|
}
|
||||||
@@ -1181,24 +1194,24 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run optional auto-update script based on configuration flags.
|
// Run optional auto-update script based on configuration flags.
|
||||||
private async runAutoUpdate(): Promise<void> {
|
private async runAutoUpdate(): Promise<number> {
|
||||||
const upd = this.config.update
|
const upd = this.config.update
|
||||||
if (!upd) return
|
if (!upd) return 0
|
||||||
const scriptRel = upd.scriptPath || 'setup/update/update.mjs'
|
const scriptRel = upd.scriptPath || 'setup/update/update.mjs'
|
||||||
const scriptAbs = path.join(process.cwd(), scriptRel)
|
const scriptAbs = path.join(process.cwd(), scriptRel)
|
||||||
if (!fs.existsSync(scriptAbs)) return
|
if (!fs.existsSync(scriptAbs)) return 0
|
||||||
|
|
||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
// Git update is enabled by default (unless explicitly set to false)
|
// Git update is enabled by default (unless explicitly set to false)
|
||||||
if (upd.git !== false) args.push('--git')
|
if (upd.git !== false) args.push('--git')
|
||||||
if (upd.docker) args.push('--docker')
|
if (upd.docker) args.push('--docker')
|
||||||
if (args.length === 0) return
|
if (args.length === 0) return 0
|
||||||
|
|
||||||
// Run update script as a child process - it will handle its own exit
|
// Run update script as a child process and capture exit code
|
||||||
await new Promise<void>((resolve) => {
|
return new Promise<number>((resolve) => {
|
||||||
const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit' })
|
const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit' })
|
||||||
child.on('close', () => resolve())
|
child.on('close', (code) => resolve(code ?? 0))
|
||||||
child.on('error', () => resolve())
|
child.on('error', () => resolve(1))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,10 @@ export class SchedulerManager {
|
|||||||
fs.mkdirSync(logDir, { recursive: true })
|
fs.mkdirSync(logDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build cron command
|
// Build cron command with proper PATH and error handling
|
||||||
const cronCommand = `${schedule} cd ${workingDir} && ${nodePath} ${path.join(workingDir, 'dist', 'index.js')} >> ${logFile} 2>&1`
|
// Important: Cron runs with minimal environment, so we need to set PATH explicitly
|
||||||
|
const nodeDir = path.dirname(nodePath)
|
||||||
|
const cronCommand = `${schedule} export PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin:$PATH && cd "${workingDir}" && "${nodePath}" "${path.join(workingDir, 'dist', 'index.js')}" >> "${logFile}" 2>&1`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if cron is installed
|
// Check if cron is installed
|
||||||
@@ -77,6 +79,14 @@ export class SchedulerManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if cron service is running (critical!)
|
||||||
|
try {
|
||||||
|
execSync('pgrep -x cron > /dev/null || pgrep -x crond > /dev/null', { stdio: 'ignore' })
|
||||||
|
} catch {
|
||||||
|
log('main', 'SCHEDULER', '⚠️ WARNING: cron service is not running! Start it with: sudo service cron start', 'warn')
|
||||||
|
log('main', 'SCHEDULER', 'Jobs will be configured but won\'t execute until cron service is started', 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
// Get current crontab
|
// Get current crontab
|
||||||
let currentCrontab = ''
|
let currentCrontab = ''
|
||||||
try {
|
try {
|
||||||
@@ -113,8 +123,11 @@ export class SchedulerManager {
|
|||||||
|
|
||||||
log('main', 'SCHEDULER', '✅ Cron job configured successfully', 'log', 'green')
|
log('main', 'SCHEDULER', '✅ Cron job configured successfully', 'log', 'green')
|
||||||
log('main', 'SCHEDULER', `Schedule: ${schedule}`, 'log')
|
log('main', 'SCHEDULER', `Schedule: ${schedule}`, 'log')
|
||||||
|
log('main', 'SCHEDULER', `Working directory: ${workingDir}`, 'log')
|
||||||
|
log('main', 'SCHEDULER', `Node path: ${nodePath}`, 'log')
|
||||||
log('main', 'SCHEDULER', `Log file: ${logFile}`, 'log')
|
log('main', 'SCHEDULER', `Log file: ${logFile}`, 'log')
|
||||||
log('main', 'SCHEDULER', 'View jobs: crontab -l', 'log')
|
log('main', 'SCHEDULER', 'View configured jobs: crontab -l', 'log')
|
||||||
|
log('main', 'SCHEDULER', 'Check cron logs: sudo tail -f /var/log/syslog | grep CRON', 'log')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('main', 'SCHEDULER', `Failed to configure cron: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
log('main', 'SCHEDULER', `Failed to configure cron: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user