fix: auth scheduler env vars, concurrency and browser stability
This commit is contained in:
@@ -7,7 +7,7 @@ import type { ServerInit } from '@sveltejs/kit';
|
||||
*
|
||||
* Environment variables:
|
||||
* - AUTH_SCHEDULER_ENABLED: Set to 'true' to enable periodic auth renewal
|
||||
* - AUTH_SCHEDULER_INTERVAL_HOURS: Hours between each renewal (default: 12)
|
||||
* - AUTH_SCHEDULER_INTERVAL_MINUTES: Minutes between each renewal (default: 720)
|
||||
*/
|
||||
export const init: ServerInit = async () => {
|
||||
console.log('[Server Init] Starting SvelteKit server...');
|
||||
|
||||
@@ -19,7 +19,16 @@ export async function initializeBrowser(): Promise<Browser> {
|
||||
}
|
||||
|
||||
export async function getBrowser(): Promise<Browser> {
|
||||
if (!browser) {
|
||||
if (!browser || !browser.isConnected()) {
|
||||
if (browser) {
|
||||
console.warn('Browser is disconnected. Re-initializing...');
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
browser = null;
|
||||
}
|
||||
return initializeBrowser();
|
||||
}
|
||||
return browser;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getBrowser } from './browser';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export interface SchedulerConfig {
|
||||
enabled: boolean;
|
||||
intervalHours: number;
|
||||
intervalMinutes: number;
|
||||
}
|
||||
|
||||
interface SchedulerState {
|
||||
@@ -23,12 +24,19 @@ const state: SchedulerState = {
|
||||
* Get scheduler configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SchedulerConfig {
|
||||
const enabled = process.env.AUTH_SCHEDULER_ENABLED === 'true';
|
||||
const intervalHours = parseInt(process.env.AUTH_SCHEDULER_INTERVAL_HOURS || '12', 10);
|
||||
const enabled = env.AUTH_SCHEDULER_ENABLED === 'true';
|
||||
let intervalMinutes = parseInt(env.AUTH_SCHEDULER_INTERVAL_MINUTES || '720', 10);
|
||||
|
||||
if (isNaN(intervalMinutes) || intervalMinutes < 15) {
|
||||
console.warn(
|
||||
`[Scheduler] Invalid or too short interval '${env.AUTH_SCHEDULER_INTERVAL_MINUTES}'. Defaulting to 720 minutes.`
|
||||
);
|
||||
intervalMinutes = 720;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
intervalHours
|
||||
intervalMinutes
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,14 +78,17 @@ async function renewInstagramAuth(): Promise<boolean> {
|
||||
|
||||
state.isRenewing = true;
|
||||
|
||||
let context = null;
|
||||
let page = null;
|
||||
|
||||
try {
|
||||
console.log('[Scheduler] Starting Instagram authentication renewal...');
|
||||
console.log(`[Scheduler] Loading existing auth from: ${authPath}`);
|
||||
|
||||
const browser = await getBrowser();
|
||||
// Load existing authentication state
|
||||
const context = await browser.newContext({ storageState: authPath });
|
||||
const page = await context.newPage();
|
||||
context = await browser.newContext({ storageState: authPath });
|
||||
page = await context.newPage();
|
||||
|
||||
// Navigate to Instagram homepage - the existing auth will be used automatically
|
||||
await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' });
|
||||
@@ -88,9 +99,6 @@ async function renewInstagramAuth(): Promise<boolean> {
|
||||
console.log('[Scheduler] Successfully authenticated with Instagram');
|
||||
} catch (e) {
|
||||
console.warn('[Scheduler] Home icon not found - session may be expired or invalid');
|
||||
await page.close();
|
||||
await context.close();
|
||||
state.isRenewing = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -105,9 +113,6 @@ async function renewInstagramAuth(): Promise<boolean> {
|
||||
// Update auth.json with refreshed session
|
||||
await context.storageState({ path: authPath });
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
|
||||
state.lastRenewalTime = Date.now();
|
||||
console.log(`[Scheduler] Instagram authentication renewed successfully at ${new Date().toISOString()}`);
|
||||
console.log(`[Scheduler] Auth state updated at: ${authPath}`);
|
||||
@@ -117,6 +122,12 @@ async function renewInstagramAuth(): Promise<boolean> {
|
||||
console.error('[Scheduler] Instagram authentication renewal failed:', error);
|
||||
return false;
|
||||
} finally {
|
||||
if (page) {
|
||||
await page.close().catch(() => {});
|
||||
}
|
||||
if (context) {
|
||||
await context.close().catch(() => {});
|
||||
}
|
||||
state.isRenewing = false;
|
||||
}
|
||||
}
|
||||
@@ -137,9 +148,9 @@ export async function startScheduler(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalMs = config.intervalHours * 60 * 60 * 1000;
|
||||
const intervalMs = config.intervalMinutes * 60 * 1000;
|
||||
|
||||
console.log(`[Scheduler] Starting authentication scheduler with ${config.intervalHours}h interval`);
|
||||
console.log(`[Scheduler] Starting authentication scheduler with ${config.intervalMinutes}min interval`);
|
||||
|
||||
// Schedule periodic renewals
|
||||
state.intervalId = setInterval(async () => {
|
||||
|
||||
@@ -2,13 +2,18 @@ import { getSchedulerStatus, startScheduler, stopScheduler } from '$lib/server/s
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock environment variables
|
||||
const setEnv = (key: string, value: string | undefined) => {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
};
|
||||
const { mockEnv } = vi.hoisted(() => {
|
||||
return {
|
||||
mockEnv: {
|
||||
AUTH_SCHEDULER_ENABLED: 'false',
|
||||
AUTH_SCHEDULER_INTERVAL_MINUTES: '720'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: mockEnv
|
||||
}));
|
||||
|
||||
// Mock the browser module
|
||||
vi.mock('$lib/server/browser', () => ({
|
||||
@@ -28,8 +33,8 @@ const mockFs = {
|
||||
describe('Scheduler Service', () => {
|
||||
beforeEach(() => {
|
||||
// Reset environment variables
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', undefined);
|
||||
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined);
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
|
||||
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '720';
|
||||
|
||||
// Clear all mocks
|
||||
vi.clearAllMocks();
|
||||
@@ -48,24 +53,24 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
it('should use default interval when AUTH_SCHEDULER_INTERVAL_HOURS is not set', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined);
|
||||
it('should use default interval when AUTH_SCHEDULER_INTERVAL_MINUTES is not set', async () => {
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
expect(status.config.intervalHours).toBe(12);
|
||||
expect(status.config.intervalMinutes).toBe(720);
|
||||
});
|
||||
|
||||
it('should parse custom interval hours from environment', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '6');
|
||||
it('should parse custom interval minutes from environment', async () => {
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '30';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
expect(status.config.intervalHours).toBe(6);
|
||||
expect(status.config.intervalMinutes).toBe(30);
|
||||
});
|
||||
|
||||
it('should disable scheduler when AUTH_SCHEDULER_ENABLED is not true', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'false');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
expect(status.config.enabled).toBe(false);
|
||||
@@ -73,7 +78,7 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
it('should parse AUTH_SCHEDULER_ENABLED as true when set to "true"', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
expect(status.config.enabled).toBe(true);
|
||||
@@ -82,7 +87,7 @@ describe('Scheduler Service', () => {
|
||||
|
||||
describe('Scheduler Lifecycle', () => {
|
||||
it('should not start when disabled', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'false');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
|
||||
|
||||
await startScheduler();
|
||||
|
||||
@@ -91,7 +96,7 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
it('should start when enabled', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
|
||||
await startScheduler();
|
||||
@@ -101,7 +106,7 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
it('should not start twice', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
|
||||
await startScheduler();
|
||||
@@ -113,7 +118,7 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
it('should stop the scheduler', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
|
||||
await startScheduler();
|
||||
@@ -132,7 +137,7 @@ describe('Scheduler Service', () => {
|
||||
|
||||
describe('Status Reporting', () => {
|
||||
it('should return scheduler status with default values', () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'false');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
|
||||
@@ -142,13 +147,13 @@ describe('Scheduler Service', () => {
|
||||
isRenewing: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
intervalHours: 12
|
||||
intervalMinutes: 720
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should report running state correctly', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
|
||||
await startScheduler();
|
||||
@@ -159,13 +164,13 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
it('should track configuration', async () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '24');
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '1440';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
|
||||
expect(status.config.enabled).toBe(true);
|
||||
expect(status.config.intervalHours).toBe(24);
|
||||
expect(status.config.intervalMinutes).toBe(1440);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,14 +192,14 @@ describe('Scheduler Service', () => {
|
||||
});
|
||||
|
||||
describe('Environment Variables', () => {
|
||||
it('should handle empty AUTH_SCHEDULER_INTERVAL_HOURS with default', () => {
|
||||
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
|
||||
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '');
|
||||
it('should handle empty AUTH_SCHEDULER_INTERVAL_MINUTES with default', () => {
|
||||
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
|
||||
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '';
|
||||
|
||||
const status = getSchedulerStatus();
|
||||
// Empty string should fall back to default due to parseInt('', 10) returning NaN
|
||||
// and the || 12 fallback
|
||||
expect(status.config.intervalHours).toBeDefined();
|
||||
// and the || 720 fallback
|
||||
expect(status.config.intervalMinutes).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user