import { execSync } from 'child_process' import fs from 'fs' import path from 'path' import os from 'os' import { log } from './Logger' import type { Config } from '../interface/Config' export class SchedulerManager { private config: Config private projectRoot: string private nodePath: string constructor(config: Config) { this.config = config this.projectRoot = process.cwd() this.nodePath = process.execPath } async setup(): Promise { const scheduling = this.config.scheduling if (!scheduling?.enabled) { // If scheduling is disabled, remove any existing scheduled tasks log('main', 'SCHEDULER', 'Automatic scheduling is disabled, checking for existing tasks to remove...') await this.remove() return } const type = scheduling.type || 'auto' const platform = os.platform() log('main', 'SCHEDULER', `Setting up automatic scheduling (type: ${type}, platform: ${platform})`) try { if (type === 'auto') { if (platform === 'win32') { await this.setupWindowsTaskScheduler() } else if (platform === 'linux' || platform === 'darwin') { await this.setupCron() } else { log('main', 'SCHEDULER', `Unsupported platform: ${platform}`, 'warn') } } else if (type === 'cron') { await this.setupCron() } else if (type === 'task-scheduler') { await this.setupWindowsTaskScheduler() } } catch (error) { log('main', 'SCHEDULER', `Failed to setup scheduler: ${error instanceof Error ? error.message : String(error)}`, 'error') } } private async setupCron(): Promise { const cronConfig = this.config.scheduling?.cron || {} const schedule = cronConfig.schedule || '0 9 * * *' const workingDir = cronConfig.workingDirectory || this.projectRoot const nodePath = cronConfig.nodePath || this.nodePath const logFile = cronConfig.logFile || path.join(workingDir, 'logs', 'rewards-cron.log') const user = cronConfig.user || '' log('main', 'SCHEDULER', `Configuring cron with schedule: ${schedule}`) // Ensure log directory exists const logDir = path.dirname(logFile) if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }) } // Build cron command with proper PATH and error handling // 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 { // Check if cron is installed try { execSync('which cron', { stdio: 'ignore' }) } catch { log('main', 'SCHEDULER', 'cron is not installed. Please install it first: sudo apt-get install cron', 'error') 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 let currentCrontab = '' try { const getCrontabCmd = user ? `crontab -u ${user} -l` : 'crontab -l' currentCrontab = execSync(getCrontabCmd, { encoding: 'utf-8' }) } catch (error) { // No existing crontab currentCrontab = '' } // Check if our job already exists const jobMarker = '# Microsoft-Rewards-Bot' if (currentCrontab.includes(jobMarker)) { log('main', 'SCHEDULER', 'Cron job already exists, updating...', 'log') // Remove old job const lines = currentCrontab.split('\n').filter(line => !line.includes(jobMarker) && !line.includes('Microsoft-Rewards-Script') ) currentCrontab = lines.join('\n') } // Add new job const newCrontab = currentCrontab.trim() + '\n' + jobMarker + '\n' + cronCommand + '\n' // Write new crontab const tempFile = path.join(os.tmpdir(), `crontab-${Date.now()}.txt`) fs.writeFileSync(tempFile, newCrontab) const setCrontabCmd = user ? `crontab -u ${user} ${tempFile}` : `crontab ${tempFile}` execSync(setCrontabCmd) // Cleanup temp file fs.unlinkSync(tempFile) log('main', 'SCHEDULER', '✅ Cron job configured successfully', 'log', 'green') 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', 'View configured jobs: crontab -l', 'log') log('main', 'SCHEDULER', 'Check cron logs: sudo tail -f /var/log/syslog | grep CRON', 'log') } catch (error) { log('main', 'SCHEDULER', `Failed to configure cron: ${error instanceof Error ? error.message : String(error)}`, 'error') } } private async setupWindowsTaskScheduler(): Promise { const taskConfig = this.config.scheduling?.taskScheduler || {} const taskName = taskConfig.taskName || 'Microsoft-Rewards-Bot' const schedule = taskConfig.schedule || '09:00' const frequency = taskConfig.frequency || 'daily' const workingDir = taskConfig.workingDirectory || this.projectRoot const runAsUser = taskConfig.runAsUser !== false const highestPrivileges = taskConfig.highestPrivileges === true log('main', 'SCHEDULER', `Configuring Windows Task Scheduler: ${taskName}`) try { // Check if task already exists const checkCmd = `schtasks /Query /TN "${taskName}" 2>nul` let taskExists = false try { execSync(checkCmd, { stdio: 'ignore' }) taskExists = true log('main', 'SCHEDULER', 'Task already exists, it will be updated', 'log') } catch { // Task doesn't exist } // Delete existing task if it exists if (taskExists) { execSync(`schtasks /Delete /TN "${taskName}" /F`, { stdio: 'ignore' }) } // Build task command const scriptPath = path.join(workingDir, 'dist', 'index.js') const action = `"${this.nodePath}" "${scriptPath}"` // Create XML for task const xmlContent = this.generateTaskSchedulerXml( taskName, action, workingDir, schedule, frequency, runAsUser, highestPrivileges ) // Save XML to temp file const tempXmlPath = path.join(os.tmpdir(), `task-${Date.now()}.xml`) fs.writeFileSync(tempXmlPath, xmlContent, 'utf-8') // Create task from XML const createCmd = `schtasks /Create /TN "${taskName}" /XML "${tempXmlPath}" /F` execSync(createCmd, { stdio: 'ignore' }) // Cleanup temp file fs.unlinkSync(tempXmlPath) log('main', 'SCHEDULER', '✅ Windows Task Scheduler configured successfully', 'log', 'green') log('main', 'SCHEDULER', `Task name: ${taskName}`, 'log') log('main', 'SCHEDULER', `Schedule: ${frequency} at ${schedule}`, 'log') log('main', 'SCHEDULER', `View task: Task Scheduler > Task Scheduler Library > ${taskName}`, 'log') } catch (error) { log('main', 'SCHEDULER', `Failed to configure Task Scheduler: ${error instanceof Error ? error.message : String(error)}`, 'error') log('main', 'SCHEDULER', 'Make sure you run this with administrator privileges', 'warn') } } private generateTaskSchedulerXml( taskName: string, action: string, workingDir: string, schedule: string, frequency: string, runAsUser: boolean, highestPrivileges: boolean ): string { const currentUser = os.userInfo().username const [hours, minutes] = schedule.split(':') const startBoundary = `2025-01-01T${hours}:${minutes}:00` let triggerXml = '' if (frequency === 'daily') { triggerXml = ` ${startBoundary} true 1 ` } else if (frequency === 'weekly') { triggerXml = ` ${startBoundary} true 1 ` } return ` Microsoft Rewards Bot - Automated task execution ${currentUser} ${triggerXml} ${runAsUser ? currentUser : 'SYSTEM'} InteractiveToken ${highestPrivileges ? 'HighestAvailable' : 'LeastPrivilege'} IgnoreNew false false true true true false false true true false false false PT2H 7 ${this.nodePath} "${path.join(workingDir, 'dist', 'index.js')}" ${workingDir} ` } async remove(): Promise { const platform = os.platform() log('main', 'SCHEDULER', 'Removing scheduled tasks...') try { if (platform === 'win32') { await this.removeWindowsTask() } else if (platform === 'linux' || platform === 'darwin') { await this.removeCron() } } catch (error) { log('main', 'SCHEDULER', `Failed to remove scheduler: ${error instanceof Error ? error.message : String(error)}`, 'error') } } private async removeCron(): Promise { try { let currentCrontab = '' try { currentCrontab = execSync('crontab -l', { encoding: 'utf-8' }) } catch { // No crontab exists, nothing to remove return } const jobMarker = '# Microsoft-Rewards-Bot' if (!currentCrontab.includes(jobMarker)) { // No job found, nothing to remove return } // Remove job const lines = currentCrontab.split('\n').filter(line => !line.includes(jobMarker) && !line.includes('Microsoft-Rewards-Script') ) const newCrontab = lines.join('\n') const tempFile = path.join(os.tmpdir(), `crontab-${Date.now()}.txt`) fs.writeFileSync(tempFile, newCrontab) execSync(`crontab ${tempFile}`) fs.unlinkSync(tempFile) log('main', 'SCHEDULER', '✅ Cron job removed successfully', 'log', 'green') } catch (error) { log('main', 'SCHEDULER', `Failed to remove cron: ${error instanceof Error ? error.message : String(error)}`, 'error') } } private async removeWindowsTask(): Promise { const taskConfig = this.config.scheduling?.taskScheduler || {} const taskName = taskConfig.taskName || 'Microsoft-Rewards-Bot' try { execSync(`schtasks /Delete /TN "${taskName}" /F`, { stdio: 'ignore' }) log('main', 'SCHEDULER', '✅ Windows Task removed successfully', 'log', 'green') } catch { // Task doesn't exist or already removed, nothing to do } } }