mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 01:06:17 +00:00
feat: add automatic task scheduling configuration for Windows and Linux
This commit is contained in:
30
README.md
30
README.md
@@ -126,6 +126,36 @@ Access at `http://localhost:3000` to:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⏰ Automatic Scheduling
|
||||||
|
|
||||||
|
Configure automatic task scheduling directly from `config.jsonc` - **perfect for Raspberry Pi!**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"scheduling": {
|
||||||
|
"enabled": true, // Just set this to true
|
||||||
|
"type": "auto", // Automatically detects Windows/Linux/Raspberry Pi
|
||||||
|
"cron": {
|
||||||
|
"schedule": "0 9 * * *" // Raspberry Pi/Linux: Daily at 9 AM
|
||||||
|
},
|
||||||
|
"taskScheduler": {
|
||||||
|
"schedule": "09:00" // Windows: Daily at 9:00
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Then simply run:**
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will automatically configure cron (Linux/Raspberry Pi) or Task Scheduler (Windows) for you!
|
||||||
|
|
||||||
|
**[📖 Full Scheduling Documentation](docs/schedule.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Docker Quick Start
|
## Docker Quick Start
|
||||||
|
|
||||||
For containerized deployment:
|
For containerized deployment:
|
||||||
|
|||||||
191
docs/schedule.md
191
docs/schedule.md
@@ -1,10 +1,155 @@
|
|||||||
# External Scheduling
|
# Automatic Task Scheduling
|
||||||
|
|
||||||
The built-in scheduler has been removed. Use your operating system or orchestrator to trigger runs with full control over timing, retries, and monitoring.
|
The bot can **automatically configure** your system's task scheduler when you run it for the first time. This works on:
|
||||||
|
- ✅ **Windows** → Windows Task Scheduler
|
||||||
|
- ✅ **Linux/Raspberry Pi** → cron
|
||||||
|
- ✅ **macOS** → cron
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Windows Task Scheduler
|
## Quick Setup (Recommended)
|
||||||
|
|
||||||
|
### 1. Edit your configuration
|
||||||
|
|
||||||
|
Open `src/config.jsonc` and find the `scheduling` section:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"scheduling": {
|
||||||
|
"enabled": true, // ← Change this to true
|
||||||
|
"type": "auto", // ← Leave as "auto" for automatic detection
|
||||||
|
|
||||||
|
// For Linux/Raspberry Pi/macOS:
|
||||||
|
"cron": {
|
||||||
|
"schedule": "0 9 * * *" // ← Daily at 9 AM (customize if needed)
|
||||||
|
},
|
||||||
|
|
||||||
|
// For Windows:
|
||||||
|
"taskScheduler": {
|
||||||
|
"schedule": "09:00", // ← Daily at 9:00 AM (customize if needed)
|
||||||
|
"frequency": "daily"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Run the bot once
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
**That's it!** The bot will automatically:
|
||||||
|
- Detect your operating system
|
||||||
|
- Configure the appropriate scheduler (cron or Task Scheduler)
|
||||||
|
- Set it up to run at your specified time
|
||||||
|
- Show you a confirmation message
|
||||||
|
|
||||||
|
### 3. Verify it worked
|
||||||
|
|
||||||
|
**Linux/Raspberry Pi/macOS:**
|
||||||
|
```bash
|
||||||
|
crontab -l
|
||||||
|
```
|
||||||
|
You should see a line with `# Microsoft-Rewards-Bot`
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
- Open Task Scheduler
|
||||||
|
- Look for "Microsoft-Rewards-Bot" in the task list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Example 1: Raspberry Pi - Run daily at 9 AM
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"scheduling": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "auto",
|
||||||
|
"cron": {
|
||||||
|
"schedule": "0 9 * * *",
|
||||||
|
"logFile": "/home/pi/rewards.log" // Optional: save logs here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Windows - Run twice daily
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"scheduling": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "auto",
|
||||||
|
"taskScheduler": {
|
||||||
|
"schedule": "09:00", // First run at 9 AM
|
||||||
|
"frequency": "daily"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For multiple times per day on Windows, you'll need to manually create additional tasks.
|
||||||
|
|
||||||
|
### Example 3: Linux - Run on weekdays only at 2:30 PM
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"scheduling": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "cron",
|
||||||
|
"cron": {
|
||||||
|
"schedule": "30 14 * * 1-5" // 2:30 PM, Monday-Friday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cron Schedule Examples
|
||||||
|
|
||||||
|
Use [crontab.guru](https://crontab.guru) to create custom schedules:
|
||||||
|
|
||||||
|
| Schedule | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `0 9 * * *` | Every day at 9:00 AM |
|
||||||
|
| `30 14 * * *` | Every day at 2:30 PM |
|
||||||
|
| `0 9,21 * * *` | Every day at 9:00 AM and 9:00 PM |
|
||||||
|
| `0 9 * * 1-5` | Weekdays at 9:00 AM (Monday-Friday) |
|
||||||
|
| `0 */6 * * *` | Every 6 hours |
|
||||||
|
| `0 8 * * 0` | Every Sunday at 8:00 AM |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Disabling Automatic Scheduling
|
||||||
|
|
||||||
|
To remove the scheduled task:
|
||||||
|
|
||||||
|
1. Set `"enabled": false` in your config
|
||||||
|
2. Run the bot once: `npm run start`
|
||||||
|
3. The scheduler will be automatically removed
|
||||||
|
|
||||||
|
Or manually remove it:
|
||||||
|
|
||||||
|
**Linux/Raspberry Pi/macOS:**
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
# Delete the line with "# Microsoft-Rewards-Bot"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
- Open Task Scheduler
|
||||||
|
- Find "Microsoft-Rewards-Bot"
|
||||||
|
- Right-click → Delete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Configuration (Advanced)
|
||||||
|
|
||||||
|
If you prefer manual setup or need more control, follow these platform-specific guides:
|
||||||
|
|
||||||
|
### Windows Task Scheduler (Manual)
|
||||||
|
|
||||||
1. Open Task Scheduler, choose **Create Basic Task...**, and name it `Microsoft Rewards Bot`.
|
1. Open Task Scheduler, choose **Create Basic Task...**, and name it `Microsoft Rewards Bot`.
|
||||||
2. Pick a trigger (daily, weekly, at startup, etc.).
|
2. Pick a trigger (daily, weekly, at startup, etc.).
|
||||||
@@ -19,7 +164,7 @@ The built-in scheduler has been removed. Use your operating system or orchestrat
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Linux / macOS (cron)
|
## Linux / macOS (cron - Manual)
|
||||||
|
|
||||||
1. Run `npm run start` once to confirm the project completes successfully.
|
1. Run `npm run start` once to confirm the project completes successfully.
|
||||||
2. Edit the crontab: `crontab -e`.
|
2. Edit the crontab: `crontab -e`.
|
||||||
@@ -33,7 +178,7 @@ Need multiple runs? Add more cron lines with different times (for example `0 9 *
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Systemd Timer (Linux alternative)
|
## Systemd Timer (Linux alternative - Manual)
|
||||||
|
|
||||||
1. Create `/etc/systemd/system/rewards-bot.service`:
|
1. Create `/etc/systemd/system/rewards-bot.service`:
|
||||||
```ini
|
```ini
|
||||||
@@ -83,10 +228,38 @@ Need multiple runs? Add more cron lines with different times (for example `0 9 *
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- Run `npm install` and `npm run build` after pulling updates.
|
**"cron is not installed"** (Linux/Raspberry Pi)
|
||||||
- Use absolute paths in scheduler commands.
|
```bash
|
||||||
- Redirect output to a log file for easier debugging.
|
sudo apt-get update
|
||||||
- Execute `npm run start` manually after configuration changes to trigger the startup validator.
|
sudo apt-get install cron
|
||||||
|
```
|
||||||
|
|
||||||
|
**"Permission denied"** (Linux/Raspberry Pi)
|
||||||
|
- The bot needs write access to crontab
|
||||||
|
- Make sure you're running as the correct user
|
||||||
|
|
||||||
|
**"Access denied"** (Windows)
|
||||||
|
- Right-click PowerShell or Command Prompt
|
||||||
|
- Choose "Run as Administrator"
|
||||||
|
- Run `npm run start` again
|
||||||
|
|
||||||
|
**Task not running at scheduled time:**
|
||||||
|
1. Check your system's time and timezone
|
||||||
|
2. Verify the schedule format is correct
|
||||||
|
3. For cron: use [crontab.guru](https://crontab.guru) to validate
|
||||||
|
4. Check logs to see if there are any errors
|
||||||
|
|
||||||
|
**Manually check if scheduler is active:**
|
||||||
|
|
||||||
|
**Linux/Raspberry Pi:**
|
||||||
|
```bash
|
||||||
|
crontab -l | grep "Microsoft-Rewards"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
schtasks /Query /TN "Microsoft-Rewards-Bot" /FO LIST
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,42 @@
|
|||||||
"scriptPath": "setup/update/update.mjs",
|
"scriptPath": "setup/update/update.mjs",
|
||||||
"autoUpdateConfig": true,
|
"autoUpdateConfig": true,
|
||||||
"autoUpdateAccounts": false
|
"autoUpdateAccounts": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// Scheduling (automatic task scheduling)
|
||||||
|
// When enabled=true, the bot will automatically configure your system scheduler on first run.
|
||||||
|
// This works on Windows (Task Scheduler), Linux/Raspberry Pi (cron), and macOS (cron).
|
||||||
|
"scheduling": {
|
||||||
|
"enabled": false, // Set to true to enable automatic scheduling
|
||||||
|
|
||||||
|
// Leave "type" as "auto" for automatic detection, or force "cron" (Linux/Raspberry Pi/macOS) or "task-scheduler" (Windows)
|
||||||
|
"type": "auto",
|
||||||
|
|
||||||
|
// Cron settings (for Linux, Raspberry Pi, macOS)
|
||||||
|
// Only used when type="auto" on Linux/macOS or type="cron"
|
||||||
|
"cron": {
|
||||||
|
"schedule": "0 9 * * *", // When to run: 9 AM daily (see https://crontab.guru to customize)
|
||||||
|
// Examples:
|
||||||
|
// "0 9 * * *" = Every day at 9:00 AM
|
||||||
|
// "30 14 * * *" = Every day at 2:30 PM
|
||||||
|
// "0 9,21 * * *" = Every day at 9:00 AM and 9:00 PM
|
||||||
|
// "0 9 * * 1-5" = Weekdays at 9:00 AM (Monday-Friday)
|
||||||
|
|
||||||
|
"workingDirectory": "", // Leave empty for auto-detection
|
||||||
|
"nodePath": "", // Leave empty for auto-detection
|
||||||
|
"logFile": "", // Optional: custom log file path (e.g., "/home/pi/rewards.log")
|
||||||
|
"user": "" // Optional: run as specific user (leave empty for current user)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Windows Task Scheduler settings
|
||||||
|
// Only used when type="auto" on Windows or type="task-scheduler"
|
||||||
|
"taskScheduler": {
|
||||||
|
"taskName": "Microsoft-Rewards-Bot", // Task name in Windows Task Scheduler
|
||||||
|
"schedule": "09:00", // Time in 24h format (e.g., "09:00", "14:30", "21:00")
|
||||||
|
"frequency": "daily", // "daily", "weekly", or "once"
|
||||||
|
"workingDirectory": "", // Leave empty for auto-detection
|
||||||
|
"runAsUser": true, // true = run as current user, false = run as SYSTEM
|
||||||
|
"highestPrivileges": false // Set to true if you need admin privileges
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { QueryDiversityEngine } from './util/QueryDiversityEngine'
|
|||||||
import JobState from './util/JobState'
|
import JobState from './util/JobState'
|
||||||
import { StartupValidator } from './util/StartupValidator'
|
import { StartupValidator } from './util/StartupValidator'
|
||||||
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
||||||
|
import { SchedulerManager } from './util/SchedulerManager'
|
||||||
|
|
||||||
import { Login } from './functions/Login'
|
import { Login } from './functions/Login'
|
||||||
import { Workers } from './functions/Workers'
|
import { Workers } from './functions/Workers'
|
||||||
@@ -128,6 +129,12 @@ export class MicrosoftRewardsBot {
|
|||||||
if (this.config.jobState?.enabled !== false) {
|
if (this.config.jobState?.enabled !== false) {
|
||||||
this.accountJobState = new JobState(this.config)
|
this.accountJobState = new JobState(this.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup automatic scheduler if enabled
|
||||||
|
if (this.config.scheduling?.enabled) {
|
||||||
|
const scheduler = new SchedulerManager(this.config)
|
||||||
|
await scheduler.setup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSkipAccount(email: string, dayKey: string): boolean {
|
private shouldSkipAccount(email: string, dayKey: string): boolean {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface Config {
|
|||||||
dryRun?: boolean; // NEW: Dry-run mode (simulate without executing)
|
dryRun?: boolean; // NEW: Dry-run mode (simulate without executing)
|
||||||
queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation
|
queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation
|
||||||
dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control
|
dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control
|
||||||
|
scheduling?: ConfigScheduling; // NEW: Automatic scheduler configuration (cron/Task Scheduler)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigSaveFingerprint {
|
export interface ConfigSaveFingerprint {
|
||||||
@@ -195,3 +196,23 @@ export interface ConfigDashboard {
|
|||||||
port?: number; // dashboard server port (default: 3000)
|
port?: number; // dashboard server port (default: 3000)
|
||||||
host?: string; // bind address (default: 127.0.0.1)
|
host?: string; // bind address (default: 127.0.0.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigScheduling {
|
||||||
|
enabled?: boolean; // enable automatic schedule configuration
|
||||||
|
type?: 'auto' | 'cron' | 'task-scheduler'; // auto-detect or force specific type
|
||||||
|
cron?: {
|
||||||
|
schedule?: string; // cron expression (default: "0 9 * * *")
|
||||||
|
workingDirectory?: string; // project root path (auto-detected if empty)
|
||||||
|
nodePath?: string; // path to node executable (auto-detected if empty)
|
||||||
|
logFile?: string; // optional log file path
|
||||||
|
user?: string; // optional specific user for crontab
|
||||||
|
};
|
||||||
|
taskScheduler?: {
|
||||||
|
taskName?: string; // task name in Windows Task Scheduler
|
||||||
|
schedule?: string; // time in 24h format (e.g., "09:00")
|
||||||
|
frequency?: 'daily' | 'weekly' | 'once'; // task frequency
|
||||||
|
workingDirectory?: string; // project root path (auto-detected if empty)
|
||||||
|
runAsUser?: boolean; // run under current user
|
||||||
|
highestPrivileges?: boolean; // request highest privileges
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
382
src/util/SchedulerManager.ts
Normal file
382
src/util/SchedulerManager.ts
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
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<void> {
|
||||||
|
const scheduling = this.config.scheduling
|
||||||
|
if (!scheduling?.enabled) {
|
||||||
|
log('main', 'SCHEDULER', 'Automatic scheduling is disabled in config', 'log')
|
||||||
|
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<void> {
|
||||||
|
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
|
||||||
|
const cronCommand = `${schedule} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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', `Log file: ${logFile}`, 'log')
|
||||||
|
log('main', 'SCHEDULER', 'View jobs: crontab -l', 'log')
|
||||||
|
} catch (error) {
|
||||||
|
log('main', 'SCHEDULER', `Failed to configure cron: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupWindowsTaskScheduler(): Promise<void> {
|
||||||
|
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 = `
|
||||||
|
<CalendarTrigger>
|
||||||
|
<StartBoundary>${startBoundary}</StartBoundary>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<ScheduleByDay>
|
||||||
|
<DaysInterval>1</DaysInterval>
|
||||||
|
</ScheduleByDay>
|
||||||
|
</CalendarTrigger>`
|
||||||
|
} else if (frequency === 'weekly') {
|
||||||
|
triggerXml = `
|
||||||
|
<CalendarTrigger>
|
||||||
|
<StartBoundary>${startBoundary}</StartBoundary>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<ScheduleByWeek>
|
||||||
|
<WeeksInterval>1</WeeksInterval>
|
||||||
|
<DaysOfWeek>
|
||||||
|
<Monday />
|
||||||
|
<Tuesday />
|
||||||
|
<Wednesday />
|
||||||
|
<Thursday />
|
||||||
|
<Friday />
|
||||||
|
<Saturday />
|
||||||
|
<Sunday />
|
||||||
|
</DaysOfWeek>
|
||||||
|
</ScheduleByWeek>
|
||||||
|
</CalendarTrigger>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
||||||
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||||
|
<RegistrationInfo>
|
||||||
|
<Description>Microsoft Rewards Bot - Automated task execution</Description>
|
||||||
|
<Author>${currentUser}</Author>
|
||||||
|
</RegistrationInfo>
|
||||||
|
<Triggers>
|
||||||
|
${triggerXml}
|
||||||
|
</Triggers>
|
||||||
|
<Principals>
|
||||||
|
<Principal id="Author">
|
||||||
|
<UserId>${runAsUser ? currentUser : 'SYSTEM'}</UserId>
|
||||||
|
<LogonType>InteractiveToken</LogonType>
|
||||||
|
<RunLevel>${highestPrivileges ? 'HighestAvailable' : 'LeastPrivilege'}</RunLevel>
|
||||||
|
</Principal>
|
||||||
|
</Principals>
|
||||||
|
<Settings>
|
||||||
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||||
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||||
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||||
|
<AllowHardTerminate>true</AllowHardTerminate>
|
||||||
|
<StartWhenAvailable>true</StartWhenAvailable>
|
||||||
|
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
|
||||||
|
<IdleSettings>
|
||||||
|
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||||
|
<RestartOnIdle>false</RestartOnIdle>
|
||||||
|
</IdleSettings>
|
||||||
|
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<Hidden>false</Hidden>
|
||||||
|
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||||
|
<WakeToRun>false</WakeToRun>
|
||||||
|
<ExecutionTimeLimit>PT2H</ExecutionTimeLimit>
|
||||||
|
<Priority>7</Priority>
|
||||||
|
</Settings>
|
||||||
|
<Actions Context="Author">
|
||||||
|
<Exec>
|
||||||
|
<Command>${this.nodePath}</Command>
|
||||||
|
<Arguments>"${path.join(workingDir, 'dist', 'index.js')}"</Arguments>
|
||||||
|
<WorkingDirectory>${workingDir}</WorkingDirectory>
|
||||||
|
</Exec>
|
||||||
|
</Actions>
|
||||||
|
</Task>`
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
try {
|
||||||
|
let currentCrontab = ''
|
||||||
|
try {
|
||||||
|
currentCrontab = execSync('crontab -l', { encoding: 'utf-8' })
|
||||||
|
} catch {
|
||||||
|
log('main', 'SCHEDULER', 'No crontab found', 'log')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobMarker = '# Microsoft-Rewards-Bot'
|
||||||
|
if (!currentCrontab.includes(jobMarker)) {
|
||||||
|
log('main', 'SCHEDULER', 'No Microsoft Rewards Bot cron job found', 'log')
|
||||||
|
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<void> {
|
||||||
|
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 (error) {
|
||||||
|
log('main', 'SCHEDULER', `Task "${taskName}" not found or already removed`, 'log')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async status(): Promise<void> {
|
||||||
|
const platform = os.platform()
|
||||||
|
log('main', 'SCHEDULER', 'Checking scheduler status...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
await this.statusWindowsTask()
|
||||||
|
} else if (platform === 'linux' || platform === 'darwin') {
|
||||||
|
await this.statusCron()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log('main', 'SCHEDULER', `Failed to check status: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async statusCron(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentCrontab = execSync('crontab -l', { encoding: 'utf-8' })
|
||||||
|
const jobMarker = '# Microsoft-Rewards-Bot'
|
||||||
|
|
||||||
|
if (currentCrontab.includes(jobMarker)) {
|
||||||
|
const lines = currentCrontab.split('\n')
|
||||||
|
const jobIndex = lines.findIndex(line => line.includes(jobMarker))
|
||||||
|
if (jobIndex >= 0 && jobIndex + 1 < lines.length) {
|
||||||
|
log('main', 'SCHEDULER', '✅ Cron job is active', 'log', 'green')
|
||||||
|
log('main', 'SCHEDULER', `Job: ${lines[jobIndex + 1]}`, 'log')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('main', 'SCHEDULER', '❌ No cron job found', 'warn')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log('main', 'SCHEDULER', '❌ No crontab configured', 'warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async statusWindowsTask(): Promise<void> {
|
||||||
|
const taskConfig = this.config.scheduling?.taskScheduler || {}
|
||||||
|
const taskName = taskConfig.taskName || 'Microsoft-Rewards-Bot'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execSync(`schtasks /Query /TN "${taskName}" /FO LIST /V`, { encoding: 'utf-8' })
|
||||||
|
if (result.includes(taskName)) {
|
||||||
|
log('main', 'SCHEDULER', '✅ Windows Task is active', 'log', 'green')
|
||||||
|
log('main', 'SCHEDULER', `Task name: ${taskName}`, 'log')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log('main', 'SCHEDULER', `❌ Task "${taskName}" not found`, 'warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user