final dev

This commit is contained in:
2026-03-11 11:56:17 +00:00
parent 2fdf54d2e6
commit 7979b353aa
18 changed files with 3651 additions and 440 deletions

View File

@@ -4,6 +4,7 @@ const path = require('path');
const { spawn } = require('child_process');
const { getDb } = require('../db/database');
const logger = require('./logger').child('SCRIPTS');
const runtimeActivityService = require('./runtimeActivityService');
const { errorToMeta } = require('../utils/errorMeta');
const SCRIPT_NAME_MAX_LENGTH = 120;
@@ -159,7 +160,7 @@ function appendWithCap(current, chunk, maxChars) {
};
}
function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd = process.cwd() }) {
function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd = process.cwd(), onChild = null }) {
return new Promise((resolve, reject) => {
const startedAt = Date.now();
const child = spawn(cmd, args, {
@@ -167,6 +168,13 @@ function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd
env: process.env,
stdio: ['ignore', 'pipe', 'pipe']
});
if (typeof onChild === 'function') {
try {
onChild(child);
} catch (_error) {
// ignore observer errors
}
}
let stdout = '';
let stderr = '';
@@ -473,18 +481,89 @@ class ScriptService {
async testScript(scriptId, options = {}) {
const script = await this.getScriptById(scriptId);
const timeoutMs = Number(options?.timeoutMs);
const effectiveTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : SCRIPT_TEST_TIMEOUT_MS;
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
};
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) {
try {
controlState.child.kill('SIGTERM');
} catch (_error) {
// ignore
}
const forceKillTimer = setTimeout(() => {
try {
if (controlState.child && !controlState.child.killed) {
controlState.child.kill('SIGKILL');
}
} catch (_error) {
// ignore
}
}, 2000);
if (typeof forceKillTimer.unref === 'function') {
forceKillTimer.unref();
}
}
return { accepted: true, message: 'Abbruch angefordert.' };
}
});
try {
const run = await runProcessCapture({
cmd: prepared.cmd,
args: prepared.args,
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : SCRIPT_TEST_TIMEOUT_MS
timeoutMs: effectiveTimeoutMs,
onChild: (child) => {
controlState.child = child;
}
});
const cancelledByUser = controlState.cancelRequested;
const success = !cancelledByUser && run.code === 0 && !run.timedOut;
runtimeActivityService.completeActivity(activityId, {
status: success ? 'success' : 'error',
success,
outcome: cancelledByUser ? 'cancelled' : (success ? 'success' : 'error'),
cancelled: cancelledByUser,
exitCode: Number.isFinite(Number(run.code)) ? Number(run.code) : null,
stdout: run.stdout || null,
stderr: run.stderr || null,
stdoutTruncated: Boolean(run.stdoutTruncated),
stderrTruncated: Boolean(run.stderrTruncated),
errorMessage: !success
? (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'})`))
: null,
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 success = run.code === 0 && !run.timedOut;
return {
scriptId: script.id,
scriptName: script.name,
@@ -498,7 +577,22 @@ class ScriptService {
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();
}
}