667 lines
20 KiB
JavaScript
667 lines
20 KiB
JavaScript
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
const { getDb } = require('../db/database');
|
|
const logger = require('./logger').child('SCRIPTS');
|
|
const settingsService = require('./settingsService');
|
|
const runtimeActivityService = require('./runtimeActivityService');
|
|
const { errorToMeta } = require('../utils/errorMeta');
|
|
|
|
const SCRIPT_NAME_MAX_LENGTH = 120;
|
|
const SCRIPT_BODY_MAX_LENGTH = 200000;
|
|
const SCRIPT_TEST_TIMEOUT_SETTING_KEY = 'script_test_timeout_ms';
|
|
const DEFAULT_SCRIPT_TEST_TIMEOUT_MS = 0;
|
|
const SCRIPT_TEST_TIMEOUT_MS = (() => {
|
|
const parsed = Number(process.env.RIPSTER_SCRIPT_TEST_TIMEOUT_MS);
|
|
if (Number.isFinite(parsed)) {
|
|
return Math.max(0, Math.trunc(parsed));
|
|
}
|
|
return DEFAULT_SCRIPT_TEST_TIMEOUT_MS;
|
|
})();
|
|
const SCRIPT_OUTPUT_MAX_CHARS = 150000;
|
|
|
|
function normalizeScriptTestTimeoutMs(rawValue, fallbackMs = SCRIPT_TEST_TIMEOUT_MS) {
|
|
const parsed = Number(rawValue);
|
|
if (Number.isFinite(parsed)) {
|
|
return Math.max(0, Math.trunc(parsed));
|
|
}
|
|
if (fallbackMs === null || fallbackMs === undefined) {
|
|
return null;
|
|
}
|
|
const parsedFallback = Number(fallbackMs);
|
|
if (Number.isFinite(parsedFallback)) {
|
|
return Math.max(0, Math.trunc(parsedFallback));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function normalizeScriptId(rawValue) {
|
|
const value = Number(rawValue);
|
|
if (!Number.isFinite(value) || value <= 0) {
|
|
return null;
|
|
}
|
|
return Math.trunc(value);
|
|
}
|
|
|
|
function normalizeScriptIdList(rawList) {
|
|
const list = Array.isArray(rawList) ? rawList : [];
|
|
const seen = new Set();
|
|
const output = [];
|
|
for (const item of list) {
|
|
const normalized = normalizeScriptId(item);
|
|
if (!normalized) {
|
|
continue;
|
|
}
|
|
const key = String(normalized);
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
output.push(normalized);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function normalizeScriptName(rawValue) {
|
|
return String(rawValue || '').trim();
|
|
}
|
|
|
|
function normalizeScriptBody(rawValue) {
|
|
return String(rawValue || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
}
|
|
|
|
function createValidationError(message, details = null) {
|
|
const error = new Error(message);
|
|
error.statusCode = 400;
|
|
if (details) {
|
|
error.details = details;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
function validateScriptPayload(payload, { partial = false } = {}) {
|
|
const body = payload && typeof payload === 'object' ? payload : {};
|
|
const hasName = Object.prototype.hasOwnProperty.call(body, 'name');
|
|
const hasScriptBody = Object.prototype.hasOwnProperty.call(body, 'scriptBody');
|
|
const normalized = {};
|
|
const errors = [];
|
|
|
|
if (!partial || hasName) {
|
|
const name = normalizeScriptName(body.name);
|
|
if (!name) {
|
|
errors.push({ field: 'name', message: 'Name darf nicht leer sein.' });
|
|
} else if (name.length > SCRIPT_NAME_MAX_LENGTH) {
|
|
errors.push({ field: 'name', message: `Name darf maximal ${SCRIPT_NAME_MAX_LENGTH} Zeichen enthalten.` });
|
|
} else {
|
|
normalized.name = name;
|
|
}
|
|
}
|
|
|
|
if (!partial || hasScriptBody) {
|
|
const scriptBody = normalizeScriptBody(body.scriptBody);
|
|
if (!scriptBody.trim()) {
|
|
errors.push({ field: 'scriptBody', message: 'Skript darf nicht leer sein.' });
|
|
} else if (scriptBody.length > SCRIPT_BODY_MAX_LENGTH) {
|
|
errors.push({ field: 'scriptBody', message: `Skript darf maximal ${SCRIPT_BODY_MAX_LENGTH} Zeichen enthalten.` });
|
|
} else {
|
|
normalized.scriptBody = scriptBody;
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
throw createValidationError('Skript ist ungültig.', errors);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function mapScriptRow(row) {
|
|
if (!row) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: Number(row.id),
|
|
name: String(row.name || ''),
|
|
scriptBody: String(row.script_body || ''),
|
|
orderIndex: Number(row.order_index || 0),
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at
|
|
};
|
|
}
|
|
|
|
function quoteForBashSingle(value) {
|
|
return `'${String(value || '').replace(/'/g, `'\"'\"'`)}'`;
|
|
}
|
|
|
|
function buildScriptEnvironment(context = {}) {
|
|
const now = new Date().toISOString();
|
|
const entries = {
|
|
RIPSTER_SCRIPT_RUN_AT: now,
|
|
RIPSTER_JOB_ID: context?.jobId ?? '',
|
|
RIPSTER_JOB_TITLE: context?.jobTitle ?? '',
|
|
RIPSTER_MODE: context?.mode ?? '',
|
|
RIPSTER_INPUT_PATH: context?.inputPath ?? '',
|
|
RIPSTER_OUTPUT_PATH: context?.outputPath ?? '',
|
|
RIPSTER_RAW_PATH: context?.rawPath ?? '',
|
|
RIPSTER_SCRIPT_ID: context?.scriptId ?? '',
|
|
RIPSTER_SCRIPT_NAME: context?.scriptName ?? '',
|
|
RIPSTER_SCRIPT_SOURCE: context?.source ?? ''
|
|
};
|
|
|
|
const output = {};
|
|
for (const [key, value] of Object.entries(entries)) {
|
|
output[key] = String(value ?? '');
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function buildScriptWrapper(scriptBody, context = {}) {
|
|
const envVars = buildScriptEnvironment(context);
|
|
const exportLines = Object.entries(envVars)
|
|
.map(([key, value]) => `export ${key}=${quoteForBashSingle(value)}`)
|
|
.join('\n');
|
|
// Wait for potential background jobs started by the script before returning.
|
|
return `${exportLines}\n\n${String(scriptBody || '')}\n\nwait\n`;
|
|
}
|
|
|
|
function appendWithCap(current, chunk, maxChars) {
|
|
const value = String(chunk || '');
|
|
if (!value) {
|
|
return { value: current, truncated: false };
|
|
}
|
|
const currentText = String(current || '');
|
|
if (currentText.length >= maxChars) {
|
|
return { value: currentText, truncated: true };
|
|
}
|
|
const available = maxChars - currentText.length;
|
|
if (value.length <= available) {
|
|
return { value: `${currentText}${value}`, truncated: false };
|
|
}
|
|
return {
|
|
value: `${currentText}${value.slice(0, available)}`,
|
|
truncated: true
|
|
};
|
|
}
|
|
|
|
function killChildProcessTree(child, signal = 'SIGTERM') {
|
|
if (!child) {
|
|
return false;
|
|
}
|
|
const pid = Number(child.pid);
|
|
if (Number.isFinite(pid) && pid > 0) {
|
|
try {
|
|
// If spawned as detached=true this targets the full process group.
|
|
process.kill(-pid, signal);
|
|
return true;
|
|
} catch (_error) {
|
|
// Fallback below.
|
|
}
|
|
}
|
|
try {
|
|
child.kill(signal);
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd = process.cwd(), onChild = null }) {
|
|
return new Promise((resolve, reject) => {
|
|
const effectiveTimeoutMs = normalizeScriptTestTimeoutMs(timeoutMs, SCRIPT_TEST_TIMEOUT_MS);
|
|
const startedAt = Date.now();
|
|
let ended = false;
|
|
const child = spawn(cmd, args, {
|
|
cwd,
|
|
env: process.env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
detached: true
|
|
});
|
|
if (typeof onChild === 'function') {
|
|
try {
|
|
onChild(child);
|
|
} catch (_error) {
|
|
// ignore observer errors
|
|
}
|
|
}
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let stdoutTruncated = false;
|
|
let stderrTruncated = false;
|
|
let timedOut = false;
|
|
|
|
let timeout = null;
|
|
if (effectiveTimeoutMs > 0) {
|
|
timeout = setTimeout(() => {
|
|
timedOut = true;
|
|
killChildProcessTree(child, 'SIGTERM');
|
|
setTimeout(() => {
|
|
if (!ended) {
|
|
killChildProcessTree(child, 'SIGKILL');
|
|
}
|
|
}, 2000);
|
|
}, effectiveTimeoutMs);
|
|
}
|
|
|
|
const onData = (streamName, chunk) => {
|
|
if (streamName === 'stdout') {
|
|
const next = appendWithCap(stdout, chunk, SCRIPT_OUTPUT_MAX_CHARS);
|
|
stdout = next.value;
|
|
stdoutTruncated = stdoutTruncated || next.truncated;
|
|
} else {
|
|
const next = appendWithCap(stderr, chunk, SCRIPT_OUTPUT_MAX_CHARS);
|
|
stderr = next.value;
|
|
stderrTruncated = stderrTruncated || next.truncated;
|
|
}
|
|
};
|
|
|
|
child.stdout?.on('data', (chunk) => onData('stdout', chunk));
|
|
child.stderr?.on('data', (chunk) => onData('stderr', chunk));
|
|
|
|
child.on('error', (error) => {
|
|
ended = true;
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
reject(error);
|
|
});
|
|
|
|
child.on('close', (code, signal) => {
|
|
ended = true;
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
const endedAt = Date.now();
|
|
resolve({
|
|
code: Number.isFinite(Number(code)) ? Number(code) : null,
|
|
signal: signal || null,
|
|
durationMs: Math.max(0, endedAt - startedAt),
|
|
timedOut,
|
|
stdout,
|
|
stderr,
|
|
stdoutTruncated,
|
|
stderrTruncated
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function resolveScriptTestTimeoutMs(options = {}) {
|
|
const timeoutFromOptions = normalizeScriptTestTimeoutMs(options?.timeoutMs, null);
|
|
if (timeoutFromOptions !== null) {
|
|
return timeoutFromOptions;
|
|
}
|
|
try {
|
|
const settingsMap = await settingsService.getSettingsMap();
|
|
return normalizeScriptTestTimeoutMs(
|
|
settingsMap?.[SCRIPT_TEST_TIMEOUT_SETTING_KEY],
|
|
SCRIPT_TEST_TIMEOUT_MS
|
|
);
|
|
} catch (error) {
|
|
logger.warn('script:test-timeout:settings-read-failed', { error: errorToMeta(error) });
|
|
return SCRIPT_TEST_TIMEOUT_MS;
|
|
}
|
|
}
|
|
|
|
class ScriptService {
|
|
async listScripts() {
|
|
const db = await getDb();
|
|
const rows = await db.all(
|
|
`
|
|
SELECT id, name, script_body, order_index, created_at, updated_at
|
|
FROM scripts
|
|
ORDER BY order_index ASC, id ASC
|
|
`
|
|
);
|
|
return rows.map(mapScriptRow);
|
|
}
|
|
|
|
async getScriptById(scriptId) {
|
|
const normalizedId = normalizeScriptId(scriptId);
|
|
if (!normalizedId) {
|
|
throw createValidationError('Ungültige scriptId.');
|
|
}
|
|
const db = await getDb();
|
|
const row = await db.get(
|
|
`
|
|
SELECT id, name, script_body, order_index, created_at, updated_at
|
|
FROM scripts
|
|
WHERE id = ?
|
|
`,
|
|
[normalizedId]
|
|
);
|
|
if (!row) {
|
|
const error = new Error(`Skript #${normalizedId} wurde nicht gefunden.`);
|
|
error.statusCode = 404;
|
|
throw error;
|
|
}
|
|
return mapScriptRow(row);
|
|
}
|
|
|
|
async createScript(payload = {}) {
|
|
const normalized = validateScriptPayload(payload, { partial: false });
|
|
const db = await getDb();
|
|
try {
|
|
const nextOrderIndex = await this._getNextOrderIndex(db);
|
|
const result = await db.run(
|
|
`
|
|
INSERT INTO scripts (name, script_body, order_index, created_at, updated_at)
|
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`,
|
|
[normalized.name, normalized.scriptBody, nextOrderIndex]
|
|
);
|
|
return this.getScriptById(result.lastID);
|
|
} catch (error) {
|
|
if (String(error?.message || '').includes('UNIQUE constraint failed')) {
|
|
throw createValidationError(`Skriptname "${normalized.name}" existiert bereits.`, [
|
|
{ field: 'name', message: 'Name muss eindeutig sein.' }
|
|
]);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updateScript(scriptId, payload = {}) {
|
|
const normalizedId = normalizeScriptId(scriptId);
|
|
if (!normalizedId) {
|
|
throw createValidationError('Ungültige scriptId.');
|
|
}
|
|
const normalized = validateScriptPayload(payload, { partial: false });
|
|
const db = await getDb();
|
|
await this.getScriptById(normalizedId);
|
|
try {
|
|
await db.run(
|
|
`
|
|
UPDATE scripts
|
|
SET
|
|
name = ?,
|
|
script_body = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`,
|
|
[normalized.name, normalized.scriptBody, normalizedId]
|
|
);
|
|
} catch (error) {
|
|
if (String(error?.message || '').includes('UNIQUE constraint failed')) {
|
|
throw createValidationError(`Skriptname "${normalized.name}" existiert bereits.`, [
|
|
{ field: 'name', message: 'Name muss eindeutig sein.' }
|
|
]);
|
|
}
|
|
throw error;
|
|
}
|
|
return this.getScriptById(normalizedId);
|
|
}
|
|
|
|
async deleteScript(scriptId) {
|
|
const normalizedId = normalizeScriptId(scriptId);
|
|
if (!normalizedId) {
|
|
throw createValidationError('Ungültige scriptId.');
|
|
}
|
|
const db = await getDb();
|
|
const existing = await this.getScriptById(normalizedId);
|
|
await db.run('DELETE FROM scripts WHERE id = ?', [normalizedId]);
|
|
return existing;
|
|
}
|
|
|
|
async getScriptsByIds(rawIds = []) {
|
|
const ids = normalizeScriptIdList(rawIds);
|
|
if (ids.length === 0) {
|
|
return [];
|
|
}
|
|
const db = await getDb();
|
|
const placeholders = ids.map(() => '?').join(', ');
|
|
const rows = await db.all(
|
|
`
|
|
SELECT id, name, script_body, order_index, created_at, updated_at
|
|
FROM scripts
|
|
WHERE id IN (${placeholders})
|
|
`,
|
|
ids
|
|
);
|
|
const byId = new Map(rows.map((row) => [Number(row.id), mapScriptRow(row)]));
|
|
return ids.map((id) => byId.get(id)).filter(Boolean);
|
|
}
|
|
|
|
async resolveScriptsByIds(rawIds = [], options = {}) {
|
|
const ids = normalizeScriptIdList(rawIds);
|
|
if (ids.length === 0) {
|
|
return [];
|
|
}
|
|
const strict = options?.strict !== false;
|
|
const scripts = await this.getScriptsByIds(ids);
|
|
if (!strict) {
|
|
return scripts;
|
|
}
|
|
const foundIds = new Set(scripts.map((item) => Number(item.id)));
|
|
const missing = ids.filter((id) => !foundIds.has(Number(id)));
|
|
if (missing.length > 0) {
|
|
throw createValidationError(`Skript(e) nicht gefunden: ${missing.join(', ')}`, [
|
|
{ field: 'selectedPostEncodeScriptIds', message: `Nicht gefunden: ${missing.join(', ')}` }
|
|
]);
|
|
}
|
|
return scripts;
|
|
}
|
|
|
|
async reorderScripts(orderedIds = []) {
|
|
const db = await getDb();
|
|
const providedIds = normalizeScriptIdList(orderedIds);
|
|
const rows = await db.all(
|
|
`
|
|
SELECT id
|
|
FROM scripts
|
|
ORDER BY order_index ASC, id ASC
|
|
`
|
|
);
|
|
if (rows.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const existingIds = rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id) && id > 0);
|
|
const existingSet = new Set(existingIds);
|
|
const used = new Set();
|
|
const nextOrder = [];
|
|
|
|
for (const id of providedIds) {
|
|
if (!existingSet.has(id) || used.has(id)) {
|
|
continue;
|
|
}
|
|
used.add(id);
|
|
nextOrder.push(id);
|
|
}
|
|
|
|
for (const id of existingIds) {
|
|
if (used.has(id)) {
|
|
continue;
|
|
}
|
|
used.add(id);
|
|
nextOrder.push(id);
|
|
}
|
|
|
|
await db.exec('BEGIN');
|
|
try {
|
|
for (let i = 0; i < nextOrder.length; i += 1) {
|
|
await db.run(
|
|
`
|
|
UPDATE scripts
|
|
SET order_index = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`,
|
|
[i + 1, nextOrder[i]]
|
|
);
|
|
}
|
|
await db.exec('COMMIT');
|
|
} catch (error) {
|
|
await db.exec('ROLLBACK');
|
|
throw error;
|
|
}
|
|
|
|
return this.listScripts();
|
|
}
|
|
|
|
async _getNextOrderIndex(db) {
|
|
const row = await db.get(
|
|
`
|
|
SELECT COALESCE(MAX(order_index), 0) AS max_order_index
|
|
FROM scripts
|
|
`
|
|
);
|
|
const maxOrder = Number(row?.max_order_index || 0);
|
|
if (!Number.isFinite(maxOrder) || maxOrder < 0) {
|
|
return 1;
|
|
}
|
|
return Math.trunc(maxOrder) + 1;
|
|
}
|
|
|
|
async createExecutableScriptFile(script, context = {}) {
|
|
const name = String(script?.name || '').trim() || `script-${script?.id || 'unknown'}`;
|
|
const scriptBody = normalizeScriptBody(script?.scriptBody);
|
|
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ripster-script-'));
|
|
const scriptPath = path.join(tempDir, 'script.sh');
|
|
const wrapped = buildScriptWrapper(scriptBody, {
|
|
...context,
|
|
scriptId: script?.id ?? context?.scriptId ?? '',
|
|
scriptName: name,
|
|
source: context?.source || 'post_encode'
|
|
});
|
|
|
|
await fs.promises.writeFile(scriptPath, wrapped, {
|
|
encoding: 'utf-8',
|
|
mode: 0o700
|
|
});
|
|
|
|
const cleanup = async () => {
|
|
try {
|
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
} catch (error) {
|
|
logger.warn('script:temp-cleanup-failed', {
|
|
scriptId: script?.id ?? null,
|
|
scriptName: name,
|
|
tempDir,
|
|
error: errorToMeta(error)
|
|
});
|
|
}
|
|
};
|
|
|
|
return {
|
|
tempDir,
|
|
scriptPath,
|
|
cmd: '/usr/bin/env',
|
|
args: ['bash', scriptPath],
|
|
argsForLog: ['bash', `<script:${name}>`],
|
|
cleanup
|
|
};
|
|
}
|
|
|
|
async testScript(scriptId, options = {}) {
|
|
const script = await this.getScriptById(scriptId);
|
|
const effectiveTimeoutMs = await resolveScriptTestTimeoutMs(options);
|
|
const prepared = await this.createExecutableScriptFile(script, {
|
|
source: 'settings_test',
|
|
mode: 'test'
|
|
});
|
|
const activityId = runtimeActivityService.startActivity('script', {
|
|
name: script.name,
|
|
source: 'settings_test',
|
|
scriptId: script.id,
|
|
currentStep: 'Skript-Test läuft'
|
|
});
|
|
const controlState = {
|
|
cancelRequested: false,
|
|
cancelReason: null,
|
|
child: null,
|
|
cancelSignalSent: false
|
|
};
|
|
runtimeActivityService.setControls(activityId, {
|
|
cancel: async (payload = {}) => {
|
|
if (controlState.cancelRequested) {
|
|
return { accepted: true, alreadyRequested: true, message: 'Abbruch bereits angefordert.' };
|
|
}
|
|
controlState.cancelRequested = true;
|
|
controlState.cancelReason = String(payload?.reason || '').trim() || 'Von Benutzer abgebrochen';
|
|
runtimeActivityService.updateActivity(activityId, {
|
|
message: 'Abbruch angefordert'
|
|
});
|
|
if (controlState.child) {
|
|
// User cancel should stop instantly.
|
|
controlState.cancelSignalSent = killChildProcessTree(controlState.child, 'SIGKILL') || controlState.cancelSignalSent;
|
|
}
|
|
return { accepted: true, message: 'Abbruch angefordert.' };
|
|
}
|
|
});
|
|
|
|
try {
|
|
const run = await runProcessCapture({
|
|
cmd: prepared.cmd,
|
|
args: prepared.args,
|
|
timeoutMs: effectiveTimeoutMs,
|
|
onChild: (child) => {
|
|
controlState.child = child;
|
|
}
|
|
});
|
|
const exitCode = Number.isFinite(Number(run.code)) ? Number(run.code) : null;
|
|
const finishedSuccessfully = exitCode === 0 && !run.timedOut;
|
|
const cancelledByUser = Boolean(controlState.cancelRequested)
|
|
&& (Boolean(controlState.cancelSignalSent) || !finishedSuccessfully);
|
|
const success = finishedSuccessfully;
|
|
const message = cancelledByUser
|
|
? (controlState.cancelReason || 'Von Benutzer abgebrochen')
|
|
: (run.timedOut
|
|
? `Skript-Test Timeout nach ${Math.round(effectiveTimeoutMs / 1000)}s`
|
|
: (success ? 'Skript-Test abgeschlossen' : `Skript-Test fehlgeschlagen (Exit ${run.code ?? 'n/a'})`));
|
|
const errorMessage = success
|
|
? null
|
|
: (cancelledByUser
|
|
? (controlState.cancelReason || 'Von Benutzer abgebrochen')
|
|
: (run.timedOut
|
|
? `Skript-Test Timeout nach ${Math.round(effectiveTimeoutMs / 1000)}s`
|
|
: `Skript-Test fehlgeschlagen (Exit ${run.code ?? 'n/a'})`));
|
|
runtimeActivityService.completeActivity(activityId, {
|
|
status: success ? 'success' : 'error',
|
|
success,
|
|
outcome: cancelledByUser ? 'cancelled' : (success ? 'success' : 'error'),
|
|
cancelled: cancelledByUser,
|
|
exitCode,
|
|
stdout: run.stdout || null,
|
|
stderr: run.stderr || null,
|
|
stdoutTruncated: Boolean(run.stdoutTruncated),
|
|
stderrTruncated: Boolean(run.stderrTruncated),
|
|
errorMessage,
|
|
message
|
|
});
|
|
return {
|
|
scriptId: script.id,
|
|
scriptName: script.name,
|
|
success,
|
|
exitCode: run.code,
|
|
signal: run.signal,
|
|
timedOut: run.timedOut,
|
|
durationMs: run.durationMs,
|
|
stdout: run.stdout,
|
|
stderr: run.stderr,
|
|
stdoutTruncated: run.stdoutTruncated,
|
|
stderrTruncated: run.stderrTruncated
|
|
};
|
|
} catch (error) {
|
|
runtimeActivityService.completeActivity(activityId, {
|
|
status: 'error',
|
|
success: false,
|
|
outcome: controlState.cancelRequested ? 'cancelled' : 'error',
|
|
cancelled: Boolean(controlState.cancelRequested),
|
|
errorMessage: controlState.cancelRequested
|
|
? (controlState.cancelReason || 'Von Benutzer abgebrochen')
|
|
: (error?.message || 'Skript-Test Fehler'),
|
|
message: controlState.cancelRequested
|
|
? (controlState.cancelReason || 'Von Benutzer abgebrochen')
|
|
: (error?.message || 'Skript-Test Fehler')
|
|
});
|
|
throw error;
|
|
} finally {
|
|
controlState.child = null;
|
|
await prepared.cleanup();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new ScriptService();
|