diff --git a/src/dashboard/BotController.ts b/src/dashboard/BotController.ts index 2d53efd..683c77e 100644 --- a/src/dashboard/BotController.ts +++ b/src/dashboard/BotController.ts @@ -1,15 +1,12 @@ import { dashboardState } from './state' - -// We'll import and run the bot directly in the same process -let botRunning = false -let botPromise: Promise | null = null +import type { MicrosoftRewardsBot } from '../index' export class BotController { - private isRunning: boolean = false + private botInstance: MicrosoftRewardsBot | null = null + private botPromise: Promise | null = null private startTime?: Date constructor() { - // Cleanup on exit process.on('exit', () => this.stop()) } @@ -26,30 +23,26 @@ export class BotController { } public async start(): Promise<{ success: boolean; error?: string; pid?: number }> { - if (this.isRunning || botRunning) { + if (this.botInstance) { return { success: false, error: 'Bot is already running' } } try { this.log('🚀 Starting bot...', 'log') - // Import the bot main logic const { MicrosoftRewardsBot } = await import('../index') - this.isRunning = true - botRunning = true + this.botInstance = new MicrosoftRewardsBot(false) this.startTime = new Date() dashboardState.setRunning(true) + dashboardState.setBotInstance(this.botInstance) - // Run the bot in the same process using the exact same logic as npm start - botPromise = (async () => { + this.botPromise = (async () => { try { - const rewardsBot = new MicrosoftRewardsBot(false) - this.log('✓ Bot initialized, starting execution...', 'log') - await rewardsBot.initialize() - await rewardsBot.run() + await this.botInstance!.initialize() + await this.botInstance!.run() this.log('✓ Bot completed successfully', 'log') } catch (error) { @@ -61,8 +54,7 @@ export class BotController { } })() - // Don't await - let it run in background - botPromise.catch(error => { + this.botPromise.catch(error => { this.log(`Bot execution failed: ${error instanceof Error ? error.message : String(error)}`, 'error') }) @@ -77,15 +69,12 @@ export class BotController { } public stop(): { success: boolean; error?: string } { - if (!this.isRunning && !botRunning) { + if (!this.botInstance) { return { success: false, error: 'Bot is not running' } } try { this.log('🛑 Stopping bot...', 'warn') - - // For now, we can't gracefully stop a running bot in the same process - // This would require refactoring the bot to support cancellation this.log('⚠ Note: Bot will complete current task before stopping', 'warn') this.cleanup() @@ -103,7 +92,6 @@ export class BotController { this.log('🔄 Restarting bot...', 'log') this.stop() - // Wait a bit before restarting return new Promise((resolve) => { setTimeout(async () => { const result = await this.start() @@ -119,7 +107,7 @@ export class BotController { startTime?: string } { return { - running: this.isRunning || botRunning, + running: !!this.botInstance, pid: process.pid, uptime: this.startTime ? Date.now() - this.startTime.getTime() : undefined, startTime: this.startTime?.toISOString() @@ -127,13 +115,12 @@ export class BotController { } private cleanup(): void { - this.isRunning = false - botRunning = false - botPromise = null + this.botInstance = null + this.botPromise = null this.startTime = undefined dashboardState.setRunning(false) + dashboardState.setBotInstance(undefined) } } -// Singleton instance export const botController = new BotController() diff --git a/src/dashboard/routes.ts b/src/dashboard/routes.ts index 7a8aeef..16d6361 100644 --- a/src/dashboard/routes.ts +++ b/src/dashboard/routes.ts @@ -1,7 +1,6 @@ import { Router, Request, Response } from 'express' import fs from 'fs' import path from 'path' -import { spawn } from 'child_process' import { dashboardState } from './state' import { loadAccounts, loadConfig, getConfigPath } from '../util/Load' import { botController } from './BotController' @@ -209,7 +208,7 @@ apiRouter.post('/restart', async (_req: Request, res: Response): Promise = } }) -// POST /api/sync/:email - Force sync single account +// POST /api/sync/:email - Force sync single account (deprecated - use full bot restart) apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise => { try { const { email } = req.params @@ -226,18 +225,10 @@ apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise { function maskUrl(url: string): string { try { const parsed = new URL(url) - return `${parsed.protocol}//${parsed.hostname.slice(0, 3)}***${parsed.pathname.slice(0, 5)}***` + const maskedHost = parsed.hostname.length > 6 + ? `${parsed.hostname.slice(0, 3)}***${parsed.hostname.slice(-3)}` + : '***' + const maskedPath = parsed.pathname.length > 5 + ? `${parsed.pathname.slice(0, 3)}***` + : '***' + return `${parsed.protocol}//${maskedHost}${maskedPath}` } catch { return '***' } diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index 89c6430..d46a617 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -108,17 +108,14 @@ export class DashboardServer { } private interceptBotLogs(): void { - // Store reference to this.clients for closure - const clients = this.clients - - // Intercept bot logs and forward to dashboard const originalLog = botLog - ;(global as Record).botLog = function( + + ;(global as Record).botLog = ( isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log' - ) { + ) => { const result = originalLog(isMobile, title, message, type) const logEntry: DashboardLog = { @@ -130,18 +127,7 @@ export class DashboardServer { } dashboardState.addLog(logEntry) - - // Broadcast to WebSocket clients - const payload = JSON.stringify({ type: 'log', log: logEntry }) - for (const client of clients) { - if (client.readyState === WebSocket.OPEN) { - try { - client.send(payload) - } catch (error) { - console.error('[Dashboard] Error sending to WebSocket client:', error) - } - } - } + this.broadcastUpdate('log', { log: logEntry }) return result } diff --git a/src/index.ts b/src/index.ts index b7e6b24..e063696 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,16 +48,12 @@ export class MicrosoftRewardsBot { public queryEngine?: QueryDiversityEngine public compromisedModeActive: boolean = false public compromisedReason?: string - // Mutex-like flag to prevent parallel execution when config.parallel is accidentally misconfigured - private isDesktopRunning: boolean = false - private isMobileRunning: boolean = false private activeWorkers: number private browserFactory: Browser = new Browser(this) private accounts: Account[] private workers: Workers private login = new Login(this) - // Buy mode (manual spending) tracking private buyMode: { enabled: boolean; email?: string } = { enabled: false } // Summary collection (per process) @@ -632,55 +628,45 @@ export class MicrosoftRewardsBot { } } else { // Sequential execution with safety checks - if (this.isDesktopRunning || this.isMobileRunning) { - log('main', 'TASK', `Race condition detected: Desktop=${this.isDesktopRunning}, Mobile=${this.isMobileRunning}. Skipping to prevent conflicts.`, 'error') - errors.push('race-condition-detected') - } else { - this.isMobile = false - this.isDesktopRunning = true - const desktopResult = await this.Desktop(account).catch(e => { + this.isMobile = false + const desktopResult = await this.Desktop(account).catch(e => { + const msg = e instanceof Error ? e.message : String(e) + this.recordRiskEvent('error', 6, `desktop:${msg}`) + log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error') + const bd = detectBanReason(e) + if (bd.status) { + banned.status = true; banned.reason = bd.reason.substring(0,200) + this.recordRiskEvent('ban_hint', 9, bd.reason) + void this.handleImmediateBanAlert(account.email, banned.reason) + } + errors.push(formatFullError('desktop', e, verbose)); return null + }) + if (desktopResult) { + desktopInitial = desktopResult.initialPoints + desktopCollected = desktopResult.collectedPoints + } + + if (!banned.status && !this.compromisedModeActive) { + this.isMobile = true + const mobileResult = await this.Mobile(account).catch((e: unknown) => { const msg = e instanceof Error ? e.message : String(e) - this.recordRiskEvent('error', 6, `desktop:${msg}`) - log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error') + this.recordRiskEvent('error', 6, `mobile:${msg}`) + log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error') const bd = detectBanReason(e) if (bd.status) { banned.status = true; banned.reason = bd.reason.substring(0,200) this.recordRiskEvent('ban_hint', 9, bd.reason) void this.handleImmediateBanAlert(account.email, banned.reason) } - errors.push(formatFullError('desktop', e, verbose)); return null + errors.push(formatFullError('mobile', e, verbose)); return null }) - if (desktopResult) { - desktopInitial = desktopResult.initialPoints - desktopCollected = desktopResult.collectedPoints - } - this.isDesktopRunning = false - - // If banned or compromised detected, skip mobile to save time - if (!banned.status && !this.compromisedModeActive) { - this.isMobile = true - this.isMobileRunning = true - const mobileResult = await this.Mobile(account).catch((e: unknown) => { - const msg = e instanceof Error ? e.message : String(e) - this.recordRiskEvent('error', 6, `mobile:${msg}`) - log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error') - const bd = detectBanReason(e) - if (bd.status) { - banned.status = true; banned.reason = bd.reason.substring(0,200) - this.recordRiskEvent('ban_hint', 9, bd.reason) - void this.handleImmediateBanAlert(account.email, banned.reason) - } - errors.push(formatFullError('mobile', e, verbose)); return null - }) - if (mobileResult) { - mobileInitial = mobileResult.initialPoints - mobileCollected = mobileResult.collectedPoints - } - this.isMobileRunning = false - } else { - const why = banned.status ? 'banned status' : 'compromised status' - log(true, 'TASK', `Skipping mobile flow for ${account.email} due to ${why}`, 'warn') + if (mobileResult) { + mobileInitial = mobileResult.initialPoints + mobileCollected = mobileResult.collectedPoints } + } else { + const why = banned.status ? 'banned status' : 'compromised status' + log(true, 'TASK', `Skipping mobile flow for ${account.email} due to ${why}`, 'warn') } }