import { db, now } from '../db.js'; import { camelJob } from '../models/mappers.js'; import { listSystemJobActions, recordSystemJobAction } from '../models/systemJobModel.js'; import { syncDynamicHostGroups } from '../models/hostGroupModel.js'; import { runCatalogChecks } from './catalogWorker.js'; import { runAutoPromotions } from './promotionWorker.js'; import { dispatchDueRunPlanSchedules } from './runPlanScheduleWorker.js'; import { cancelExecutionJob, appendJobLog, activeExecutionJobIds } from './powershellRunner.js'; const backgroundRuns = new Map(); const BACKGROUND_JOBS = [ { id: 'worker:host-group-sync', name: 'Dynamic Host Group Sync', category: 'Inventory', description: 'Reconciles rule-based Host Groups against the current Host Library.', run: async ({ signal } = {}) => syncDynamicHostGroups({ signal }) }, { id: 'worker:catalog-auto-check', name: 'Application Catalog Auto-Check', category: 'Applications', description: 'Checks opted-in application records for newer upstream versions.', run: async ({ signal } = {}) => runCatalogChecks({ signal }) }, { id: 'worker:intune-auto-promote', name: 'Intune Auto-Promotion', category: 'Intune', description: 'Promotes deployment rings when configured soak windows elapse.', run: async ({ signal } = {}) => runAutoPromotions({ signal }) }, { id: 'worker:runplan-schedule-dispatch', name: 'RunPlan Schedule Dispatch', category: 'Scheduling', description: 'Queues due automated RunPlan schedules and records dispatch history.', run: async ({ signal } = {}) => dispatchDueRunPlanSchedules({ signal }) } ]; export function listSystemJobs() { return { jobs: [...executionJobs(), ...backgroundJobs()], actions: listSystemJobActions(200), activeExecutionJobIds: activeExecutionJobIds() }; } export async function runSystemJob(targetId, actorUserId) { const definition = BACKGROUND_JOBS.find((job) => job.id === targetId); if (!definition) { const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'run-now', status: 'failed', message: 'System job not found.', actorUserId }); return { ok: false, action, error: 'System job not found.' }; } if (backgroundRuns.get(targetId)?.running) { const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'run-now', status: 'skipped', message: `${definition.name} is already running.`, actorUserId }); return { ok: false, action, error: `${definition.name} is already running.` }; } const controller = new AbortController(); backgroundRuns.set(targetId, { running: true, cancelRequested: false, startedAt: now(), lastError: '', lastSummary: null, controller }); const startAction = recordSystemJobAction({ targetType: 'system', targetId, action: 'run-now', status: 'started', message: `${definition.name} started by operator.`, actorUserId }); try { const summary = await definition.run({ signal: controller.signal }); const canceled = Boolean(backgroundRuns.get(targetId)?.cancelRequested || controller.signal.aborted); backgroundRuns.set(targetId, { running: false, cancelRequested: false, startedAt: '', lastRunAt: now(), lastError: canceled ? 'Canceled by operator.' : '', lastSummary: summary }); const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'run-now', status: canceled ? 'canceled' : 'success', message: canceled ? `${definition.name} canceled by operator.` : `${definition.name} completed.`, actorUserId, metadata: { summary, startActionId: startAction.id } }); return { ok: !canceled, action, summary, canceled }; } catch (error) { const canceled = Boolean(backgroundRuns.get(targetId)?.cancelRequested || controller.signal.aborted); backgroundRuns.set(targetId, { running: false, cancelRequested: false, startedAt: '', lastRunAt: now(), lastError: canceled ? 'Canceled by operator.' : error.message, lastSummary: null }); const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'run-now', status: canceled ? 'canceled' : 'failed', message: canceled ? `${definition.name} canceled by operator.` : error.message, actorUserId, metadata: { startActionId: startAction.id } }); return { ok: false, action, error: canceled ? 'Canceled by operator.' : error.message, canceled }; } } export function cancelSystemJob(targetId, actorUserId, reason = '') { if (targetId.startsWith('worker:')) { const definition = BACKGROUND_JOBS.find((job) => job.id === targetId); const state = backgroundRuns.get(targetId); if (!definition || !state?.running) { const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'cancel', status: 'failed', message: definition ? `${definition.name} is not running.` : 'System job not found.', actorUserId, metadata: { reason } }); return { ok: false, action, error: action.message }; } state.cancelRequested = true; state.controller?.abort?.(new Error(reason || 'Canceled by operator.')); backgroundRuns.set(targetId, state); const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'cancel', status: 'success', message: `Cancel requested for ${definition.name}.`, actorUserId, metadata: { reason } }); return { ok: true, action, result: { canceled: true, targetType: 'background' } }; } if (!targetId.startsWith('job_')) { const action = recordSystemJobAction({ targetType: 'system', targetId, action: 'cancel', status: 'failed', message: 'Only running script execution jobs can be canceled.', actorUserId, metadata: { reason } }); return { ok: false, action, error: 'Only running script execution jobs can be canceled.' }; } const result = cancelExecutionJob(targetId, { actorUserId, reason }); const status = result.canceled ? 'success' : 'failed'; const message = result.canceled ? `Cancel requested; signaled ${result.signaledProcesses || 0} running process(es).` : result.reason; const action = recordSystemJobAction({ jobId: targetId, targetType: 'execution', targetId, action: 'cancel', status, message, actorUserId, metadata: { reason, result } }); if (result.canceled) appendJobLog(targetId, 'system', `Job management action logged: cancel (${action.id}).`, null); return { ok: result.canceled, action, result, error: result.canceled ? null : result.reason }; } function executionJobs() { const rows = db.prepare(` SELECT j.*, r.name AS runplan_name, s.name AS script_name, COUNT(jh.id) AS host_count, SUM(CASE WHEN jh.status = 'running' THEN 1 ELSE 0 END) AS running_hosts, SUM(CASE WHEN jh.status = 'queued' THEN 1 ELSE 0 END) AS queued_hosts FROM jobs j LEFT JOIN runplans r ON r.id = j.runplan_id LEFT JOIN scripts s ON s.id = j.script_id LEFT JOIN job_hosts jh ON jh.job_id = j.id GROUP BY j.id ORDER BY j.created_at DESC LIMIT 200 `).all(); return rows.map((row) => { const job = camelJob(row); const running = ['queued', 'running'].includes(job.status); return { id: job.id, type: 'execution', category: 'Script Execution', name: job.runplanName || job.scriptName || job.id, description: job.runplanName ? `RunPlan ${job.runplanName}` : `Script test ${job.scriptName || job.id}`, status: job.status, canCancel: running, canRunNow: false, startedAt: job.startedAt || '', endedAt: job.endedAt || '', createdAt: job.createdAt, hostCount: row.host_count || 0, runningHosts: row.running_hosts || 0, queuedHosts: row.queued_hosts || 0, metadata: { runplanId: job.runplanId, scriptId: job.scriptId, triggeredBy: job.triggeredBy } }; }); } function backgroundJobs() { return BACKGROUND_JOBS.map((job) => { const state = backgroundRuns.get(job.id) || {}; return { id: job.id, type: 'background', category: job.category, name: job.name, description: job.description, status: state.cancelRequested ? 'canceling' : state.running ? 'running' : 'idle', canCancel: Boolean(state.running), canRunNow: !state.running, startedAt: state.startedAt || '', endedAt: '', createdAt: '', hostCount: 0, runningHosts: 0, queuedHosts: 0, metadata: { lastRunAt: state.lastRunAt || '', lastError: state.lastError || '', lastSummary: state.lastSummary || null } }; }); }