final dev
This commit is contained in:
@@ -37,7 +37,8 @@ function spawnTrackedProcess({
|
|||||||
const child = spawn(cmd, args, {
|
const child = spawn(cmd, args, {
|
||||||
cwd,
|
cwd,
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onStart) {
|
if (onStart) {
|
||||||
@@ -72,6 +73,23 @@ function spawnTrackedProcess({
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cancelCalled = false;
|
let cancelCalled = false;
|
||||||
|
const killProcessTree = (signal) => {
|
||||||
|
const pid = Number(child.pid);
|
||||||
|
if (Number.isFinite(pid) && pid > 0) {
|
||||||
|
try {
|
||||||
|
process.kill(-pid, signal);
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
// fallback below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
child.kill(signal);
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
if (cancelCalled) {
|
if (cancelCalled) {
|
||||||
return;
|
return;
|
||||||
@@ -79,17 +97,8 @@ function spawnTrackedProcess({
|
|||||||
cancelCalled = true;
|
cancelCalled = true;
|
||||||
|
|
||||||
logger.warn('spawn:cancel:requested', { cmd, args, context, pid: child.pid });
|
logger.warn('spawn:cancel:requested', { cmd, args, context, pid: child.pid });
|
||||||
child.kill('SIGINT');
|
// Instant cancel by user request.
|
||||||
|
killProcessTree('SIGKILL');
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
process.kill(child.pid, 0);
|
|
||||||
logger.warn('spawn:cancel:force-kill', { cmd, args, context, pid: child.pid });
|
|
||||||
child.kill('SIGKILL');
|
|
||||||
} catch (_e) {
|
|
||||||
// Process already terminated
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -54,27 +54,26 @@ function mapStepRow(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function terminateChildProcess(child) {
|
function terminateChildProcess(child, { immediate = false } = {}) {
|
||||||
if (!child) {
|
if (!child) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const signal = immediate ? 'SIGKILL' : 'SIGTERM';
|
||||||
|
const pid = Number(child.pid);
|
||||||
|
if (Number.isFinite(pid) && pid > 0) {
|
||||||
|
try {
|
||||||
|
// For detached children this targets the full process group.
|
||||||
|
process.kill(-pid, signal);
|
||||||
|
return;
|
||||||
|
} catch (_error) {
|
||||||
|
// Fall through to direct child signal.
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
child.kill('SIGTERM');
|
child.kill(signal);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const forceKillTimer = setTimeout(() => {
|
|
||||||
try {
|
|
||||||
if (!child.killed) {
|
|
||||||
child.kill('SIGKILL');
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
if (typeof forceKillTimer.unref === 'function') {
|
|
||||||
forceKillTimer.unref();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateSteps(rawSteps) {
|
function validateSteps(rawSteps) {
|
||||||
@@ -459,7 +458,7 @@ class ScriptChainService {
|
|||||||
controlState.activeWaitResolve('cancel');
|
controlState.activeWaitResolve('cancel');
|
||||||
} else if (controlState.currentStepType === STEP_TYPE_SCRIPT && controlState.activeChild) {
|
} else if (controlState.currentStepType === STEP_TYPE_SCRIPT && controlState.activeChild) {
|
||||||
controlState.activeChildTermination = 'cancel';
|
controlState.activeChildTermination = 'cancel';
|
||||||
terminateChildProcess(controlState.activeChild);
|
terminateChildProcess(controlState.activeChild, { immediate: true });
|
||||||
}
|
}
|
||||||
return { accepted: true, message: 'Abbruch angefordert.' };
|
return { accepted: true, message: 'Abbruch angefordert.' };
|
||||||
};
|
};
|
||||||
@@ -483,7 +482,7 @@ class ScriptChainService {
|
|||||||
}
|
}
|
||||||
if (controlState.currentStepType === STEP_TYPE_SCRIPT && controlState.activeChild) {
|
if (controlState.currentStepType === STEP_TYPE_SCRIPT && controlState.activeChild) {
|
||||||
controlState.activeChildTermination = 'skip';
|
controlState.activeChildTermination = 'skip';
|
||||||
terminateChildProcess(controlState.activeChild);
|
terminateChildProcess(controlState.activeChild, { immediate: true });
|
||||||
runtimeActivityService.updateActivity(activityId, {
|
runtimeActivityService.updateActivity(activityId, {
|
||||||
message: 'Nächster Schritt angefordert (aktuelles Skript wird übersprungen)'
|
message: 'Nächster Schritt angefordert (aktuelles Skript wird übersprungen)'
|
||||||
});
|
});
|
||||||
@@ -619,7 +618,8 @@ class ScriptChainService {
|
|||||||
const run = await new Promise((resolve, reject) => {
|
const run = await new Promise((resolve, reject) => {
|
||||||
const child = spawn(prepared.cmd, prepared.args, {
|
const child = spawn(prepared.cmd, prepared.args, {
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: true
|
||||||
});
|
});
|
||||||
controlState.activeChild = child;
|
controlState.activeChild = child;
|
||||||
controlState.activeChildTermination = null;
|
controlState.activeChildTermination = null;
|
||||||
|
|||||||
@@ -160,13 +160,37 @@ function appendWithCap(current, chunk, maxChars) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }) {
|
function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd = process.cwd(), onChild = null }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
let ended = false;
|
||||||
const child = spawn(cmd, args, {
|
const child = spawn(cmd, args, {
|
||||||
cwd,
|
cwd,
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: true
|
||||||
});
|
});
|
||||||
if (typeof onChild === 'function') {
|
if (typeof onChild === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -184,10 +208,10 @@ function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd
|
|||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
timedOut = true;
|
timedOut = true;
|
||||||
child.kill('SIGTERM');
|
killChildProcessTree(child, 'SIGTERM');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!child.killed) {
|
if (!ended) {
|
||||||
child.kill('SIGKILL');
|
killChildProcessTree(child, 'SIGKILL');
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}, Math.max(1000, Number(timeoutMs || SCRIPT_TEST_TIMEOUT_MS)));
|
}, Math.max(1000, Number(timeoutMs || SCRIPT_TEST_TIMEOUT_MS)));
|
||||||
@@ -208,11 +232,13 @@ function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd
|
|||||||
child.stderr?.on('data', (chunk) => onData('stderr', chunk));
|
child.stderr?.on('data', (chunk) => onData('stderr', chunk));
|
||||||
|
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
|
ended = true;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code, signal) => {
|
child.on('close', (code, signal) => {
|
||||||
|
ended = true;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const endedAt = Date.now();
|
const endedAt = Date.now();
|
||||||
resolve({
|
resolve({
|
||||||
@@ -508,23 +534,8 @@ class ScriptService {
|
|||||||
message: 'Abbruch angefordert'
|
message: 'Abbruch angefordert'
|
||||||
});
|
});
|
||||||
if (controlState.child) {
|
if (controlState.child) {
|
||||||
try {
|
// User cancel should stop instantly.
|
||||||
controlState.child.kill('SIGTERM');
|
killChildProcessTree(controlState.child, 'SIGKILL');
|
||||||
} 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.' };
|
return { accepted: true, message: 'Abbruch angefordert.' };
|
||||||
}
|
}
|
||||||
@@ -539,30 +550,34 @@ class ScriptService {
|
|||||||
controlState.child = child;
|
controlState.child = child;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const cancelledByUser = controlState.cancelRequested;
|
const exitCode = Number.isFinite(Number(run.code)) ? Number(run.code) : null;
|
||||||
const success = !cancelledByUser && run.code === 0 && !run.timedOut;
|
const finishedSuccessfully = exitCode === 0 && !run.timedOut;
|
||||||
|
const cancelledByUser = Boolean(controlState.cancelRequested) && !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, {
|
runtimeActivityService.completeActivity(activityId, {
|
||||||
status: success ? 'success' : 'error',
|
status: success ? 'success' : 'error',
|
||||||
success,
|
success,
|
||||||
outcome: cancelledByUser ? 'cancelled' : (success ? 'success' : 'error'),
|
outcome: cancelledByUser ? 'cancelled' : (success ? 'success' : 'error'),
|
||||||
cancelled: cancelledByUser,
|
cancelled: cancelledByUser,
|
||||||
exitCode: Number.isFinite(Number(run.code)) ? Number(run.code) : null,
|
exitCode,
|
||||||
stdout: run.stdout || null,
|
stdout: run.stdout || null,
|
||||||
stderr: run.stderr || null,
|
stderr: run.stderr || null,
|
||||||
stdoutTruncated: Boolean(run.stdoutTruncated),
|
stdoutTruncated: Boolean(run.stdoutTruncated),
|
||||||
stderrTruncated: Boolean(run.stderrTruncated),
|
stderrTruncated: Boolean(run.stderrTruncated),
|
||||||
errorMessage: !success
|
errorMessage,
|
||||||
? (cancelledByUser
|
message
|
||||||
? (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'})`))
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
scriptId: script.id,
|
scriptId: script.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user