258 lines
8.5 KiB
JavaScript
258 lines
8.5 KiB
JavaScript
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 { 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 })
|
|
}
|
|
];
|
|
|
|
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
|
|
}
|
|
};
|
|
});
|
|
}
|