@testdriverai/runner 7.11.188-test → 7.11.189-test
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ably-service.js +141 -100
- package/lib/automation.js +61 -19
- package/package.json +1 -1
package/lib/ably-service.js
CHANGED
|
@@ -25,6 +25,15 @@ const https = require('https');
|
|
|
25
25
|
const { EventEmitter } = require('events');
|
|
26
26
|
const { maskCommandPayload } = require('./mask-command');
|
|
27
27
|
|
|
28
|
+
// Coalescing ceiling for queued exec.output chunks. Ably's hard message limit
|
|
29
|
+
// is 64KB; stay well under it to leave room for the JSON envelope + escaping.
|
|
30
|
+
const EXEC_MESSAGE_MAX_BYTES = 48 * 1024;
|
|
31
|
+
|
|
32
|
+
// Debug log batching: one message per interval, with a cap on how many lines
|
|
33
|
+
// can pile up between flushes.
|
|
34
|
+
const RUNNER_LOG_FLUSH_MS = 250;
|
|
35
|
+
const RUNNER_LOG_MAX_PENDING = 200;
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
38
|
* Get the local runner version from package.json.
|
|
30
39
|
*/
|
|
@@ -206,6 +215,19 @@ class AblyService extends EventEmitter {
|
|
|
206
215
|
this._statsInterval = null;
|
|
207
216
|
this._closed = false; // set to true in close() to suppress post-close events
|
|
208
217
|
this._inFlight = new Map(); // requestId → { type, startedAt }; commands currently executing
|
|
218
|
+
|
|
219
|
+
// Ably enforces `channel.maxRate` (50 msg/sec) across *every* publisher on
|
|
220
|
+
// the session channel — the SDK and the API publish here too. The runner
|
|
221
|
+
// takes a 20 msg/sec slice and funnels all of its publishes through one
|
|
222
|
+
// queue so bursts of responses/logs/exec output can't blow the budget.
|
|
223
|
+
this._publishQueue = [];
|
|
224
|
+
this._publishDraining = false;
|
|
225
|
+
this._publishLastTime = 0;
|
|
226
|
+
this._publishMinIntervalMs = Number(process.env.TD_RUNNER_PUBLISH_INTERVAL_MS) || 50;
|
|
227
|
+
|
|
228
|
+
// Coalesced runner.log batching (debug mode only).
|
|
229
|
+
this._pendingLogs = [];
|
|
230
|
+
this._logFlushTimer = null;
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
/**
|
|
@@ -275,48 +297,25 @@ class AblyService extends EventEmitter {
|
|
|
275
297
|
this.emit('log', `[stats] connection=${connState} | sandbox=${this._sandboxId} | channel=${chState}`);
|
|
276
298
|
}, 10000);
|
|
277
299
|
|
|
278
|
-
// Forward debug logs from automation to SDK (only when debug mode is enabled)
|
|
300
|
+
// Forward debug logs from automation to SDK (only when debug mode is enabled).
|
|
301
|
+
// Log lines are coalesced per level so a burst becomes one message.
|
|
279
302
|
this._debugMode = false;
|
|
280
|
-
|
|
303
|
+
const forwardLog = (level) => (message) => {
|
|
281
304
|
if (!this._debugMode) return;
|
|
282
|
-
this.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}).catch(() => { }); // best-effort
|
|
288
|
-
});
|
|
289
|
-
this._automation.on('warn', (message) => {
|
|
290
|
-
if (!this._debugMode) return;
|
|
291
|
-
this._sendResponse({
|
|
292
|
-
type: 'runner.log',
|
|
293
|
-
level: 'warn',
|
|
294
|
-
message,
|
|
295
|
-
timestamp: Date.now(),
|
|
296
|
-
}).catch(() => { }); // best-effort
|
|
297
|
-
});
|
|
298
|
-
this._automation.on('error', (message) => {
|
|
299
|
-
if (!this._debugMode) return;
|
|
300
|
-
this._sendResponse({
|
|
301
|
-
type: 'runner.log',
|
|
302
|
-
level: 'error',
|
|
303
|
-
message: typeof message === 'string' ? message : message.message || String(message),
|
|
304
|
-
timestamp: Date.now(),
|
|
305
|
-
}).catch(() => { }); // best-effort
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// Forward exec streaming chunks to SDK with rate limiting.
|
|
309
|
-
// Exec output can produce many chunks rapidly (e.g. verbose commands);
|
|
310
|
-
// throttle to avoid hitting Ably's 50 msg/sec per-connection limit.
|
|
311
|
-
this._execOutputLastTime = 0;
|
|
312
|
-
this._execOutputMinIntervalMs = 50; // 20 msg/sec max for exec.output
|
|
313
|
-
this._execOutputQueue = []; // queued chunks waiting to send
|
|
314
|
-
this._execOutputDraining = false;
|
|
305
|
+
this._sendRunnerLog(level, message);
|
|
306
|
+
};
|
|
307
|
+
this._automation.on('log', forwardLog('info'));
|
|
308
|
+
this._automation.on('warn', forwardLog('warn'));
|
|
309
|
+
this._automation.on('error', forwardLog('error'));
|
|
315
310
|
|
|
316
311
|
this._automation.on('exec.output', ({ requestId, chunk }) => {
|
|
317
312
|
if (this._closed) return; // don't enqueue once we're tearing down
|
|
318
|
-
this.
|
|
319
|
-
|
|
313
|
+
this._sendResponse({
|
|
314
|
+
type: 'exec.output',
|
|
315
|
+
requestId,
|
|
316
|
+
chunk,
|
|
317
|
+
timestamp: Date.now(),
|
|
318
|
+
}).catch(() => { }); // best-effort
|
|
320
319
|
});
|
|
321
320
|
|
|
322
321
|
// Subscribe to commands — save subscription ref for historyBeforeSubscribe()
|
|
@@ -560,7 +559,7 @@ class AblyService extends EventEmitter {
|
|
|
560
559
|
remoteVersion: this._updateInfo.remoteVersion,
|
|
561
560
|
};
|
|
562
561
|
}
|
|
563
|
-
await this.
|
|
562
|
+
await this._enqueuePublish('control', readyPayload);
|
|
564
563
|
this.emit('log', 'Published runner.ready signal');
|
|
565
564
|
|
|
566
565
|
// Re-publish runner.ready every 3s for up to 60s.
|
|
@@ -569,8 +568,7 @@ class AblyService extends EventEmitter {
|
|
|
569
568
|
// the SDK catches at least one live runner.ready message.
|
|
570
569
|
this._readyInterval = setInterval(async () => {
|
|
571
570
|
try {
|
|
572
|
-
readyPayload
|
|
573
|
-
await this._sessionChannel.publish('control', readyPayload);
|
|
571
|
+
await this._enqueuePublish('control', { ...readyPayload, timestamp: Date.now() });
|
|
574
572
|
this.emit('log', 'Re-published runner.ready signal');
|
|
575
573
|
} catch (err) {
|
|
576
574
|
this.emit('log', `Failed to re-publish runner.ready: ${err.message}`);
|
|
@@ -598,62 +596,115 @@ class AblyService extends EventEmitter {
|
|
|
598
596
|
}
|
|
599
597
|
|
|
600
598
|
/**
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
599
|
+
* Queue a publish on the session channel. All runner publishes go through
|
|
600
|
+
* here so the runner never exceeds its slice of the shared channel rate.
|
|
601
|
+
*
|
|
602
|
+
* Adjacent exec.output chunks for the same requestId are coalesced while
|
|
603
|
+
* they sit in the queue, so a burst of output costs one message instead of
|
|
604
|
+
* many (bounded by Ably's 64KB message limit).
|
|
605
|
+
*/
|
|
606
|
+
_enqueuePublish(eventName, message) {
|
|
607
|
+
if (this._closed) return Promise.resolve();
|
|
608
|
+
|
|
609
|
+
if (message.type === 'exec.output') {
|
|
610
|
+
const tail = this._publishQueue[this._publishQueue.length - 1];
|
|
611
|
+
if (
|
|
612
|
+
tail &&
|
|
613
|
+
tail.message.type === 'exec.output' &&
|
|
614
|
+
tail.message.requestId === message.requestId &&
|
|
615
|
+
tail.message.chunk.length + message.chunk.length <= EXEC_MESSAGE_MAX_BYTES
|
|
616
|
+
) {
|
|
617
|
+
tail.message.chunk += message.chunk;
|
|
618
|
+
return Promise.resolve();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return new Promise((resolve) => {
|
|
623
|
+
this._publishQueue.push({ eventName, message, resolve });
|
|
624
|
+
this._drainPublishQueue();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Drain the publish queue at the configured interval.
|
|
604
630
|
*/
|
|
605
|
-
async
|
|
606
|
-
if (this.
|
|
607
|
-
this.
|
|
631
|
+
async _drainPublishQueue() {
|
|
632
|
+
if (this._publishDraining) return; // already draining
|
|
633
|
+
this._publishDraining = true;
|
|
608
634
|
|
|
609
635
|
try {
|
|
610
|
-
while (this.
|
|
636
|
+
while (this._publishQueue.length > 0) {
|
|
611
637
|
// Stop draining once close() has started tearing down the channel —
|
|
612
638
|
// a publish landing after detach() can make Ably silently
|
|
613
639
|
// re-attach the channel, doubling channel-open/close lifecycle
|
|
614
640
|
// events for what should be one clean session teardown.
|
|
615
|
-
if (this._closed)
|
|
616
|
-
this._execOutputQueue.length = 0;
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Rate limit: wait if needed
|
|
621
|
-
const now = Date.now();
|
|
622
|
-
const elapsed = now - this._execOutputLastTime;
|
|
623
|
-
if (elapsed < this._execOutputMinIntervalMs) {
|
|
624
|
-
await new Promise((resolve) => setTimeout(resolve, this._execOutputMinIntervalMs - elapsed));
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (this._closed) {
|
|
628
|
-
this._execOutputQueue.length = 0;
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
641
|
+
if (this._closed) break;
|
|
631
642
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const { requestId, chunk } = this._execOutputQueue.shift();
|
|
636
|
-
if (!batch[requestId]) batch[requestId] = '';
|
|
637
|
-
batch[requestId] += chunk;
|
|
643
|
+
const elapsed = Date.now() - this._publishLastTime;
|
|
644
|
+
if (elapsed < this._publishMinIntervalMs) {
|
|
645
|
+
await new Promise((resolve) => setTimeout(resolve, this._publishMinIntervalMs - elapsed));
|
|
638
646
|
}
|
|
647
|
+
if (this._closed) break;
|
|
639
648
|
|
|
640
|
-
|
|
649
|
+
const item = this._publishQueue.shift();
|
|
650
|
+
if (!item) break;
|
|
651
|
+
this._publishLastTime = Date.now();
|
|
641
652
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
this.
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
chunk,
|
|
648
|
-
timestamp: Date.now(),
|
|
649
|
-
}).catch(() => { }); // best-effort
|
|
653
|
+
try {
|
|
654
|
+
this.emit('log', `Publishing ${item.eventName}: type=${item.message.type || 'unknown'} (requestId=${item.message.requestId || 'none'})`);
|
|
655
|
+
await this._sessionChannel.publish(item.eventName, item.message);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
this.emit('log', `Failed to publish ${item.eventName}: ${err.message}`);
|
|
650
658
|
}
|
|
659
|
+
item.resolve();
|
|
651
660
|
}
|
|
652
661
|
} finally {
|
|
653
|
-
this.
|
|
662
|
+
this._publishDraining = false;
|
|
663
|
+
if (this._closed) {
|
|
664
|
+
for (const item of this._publishQueue) item.resolve();
|
|
665
|
+
this._publishQueue.length = 0;
|
|
666
|
+
}
|
|
654
667
|
}
|
|
655
668
|
}
|
|
656
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Queue a runner.log line. Lines are batched into a single message per
|
|
672
|
+
* flush interval so verbose debug output can't swamp the channel budget.
|
|
673
|
+
*/
|
|
674
|
+
_sendRunnerLog(level, message) {
|
|
675
|
+
const text = typeof message === 'string' ? message : (message && message.message) || String(message);
|
|
676
|
+
|
|
677
|
+
if (this._pendingLogs.length >= RUNNER_LOG_MAX_PENDING) {
|
|
678
|
+
this._pendingLogs[this._pendingLogs.length - 1] = {
|
|
679
|
+
level: 'warn',
|
|
680
|
+
message: `[runner.log dropped — more than ${RUNNER_LOG_MAX_PENDING} lines queued]`,
|
|
681
|
+
timestamp: Date.now(),
|
|
682
|
+
};
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
this._pendingLogs.push({ level, message: text, timestamp: Date.now() });
|
|
687
|
+
|
|
688
|
+
if (this._logFlushTimer) return;
|
|
689
|
+
this._logFlushTimer = setTimeout(() => {
|
|
690
|
+
this._logFlushTimer = null;
|
|
691
|
+
this._flushRunnerLogs();
|
|
692
|
+
}, RUNNER_LOG_FLUSH_MS);
|
|
693
|
+
if (this._logFlushTimer.unref) this._logFlushTimer.unref();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_flushRunnerLogs() {
|
|
697
|
+
if (this._closed || this._pendingLogs.length === 0) return;
|
|
698
|
+
const lines = this._pendingLogs;
|
|
699
|
+
this._pendingLogs = [];
|
|
700
|
+
this._sendResponse({
|
|
701
|
+
type: 'runner.log',
|
|
702
|
+
level: lines.some((l) => l.level === 'error') ? 'error' : lines[0].level,
|
|
703
|
+
message: lines.map((l) => l.message).join('\n'),
|
|
704
|
+
timestamp: Date.now(),
|
|
705
|
+
}).catch(() => { }); // best-effort
|
|
706
|
+
}
|
|
707
|
+
|
|
657
708
|
/**
|
|
658
709
|
* Recover missed messages after a channel discontinuity.
|
|
659
710
|
* Uses historyBeforeSubscribe() on each subscription, which guarantees
|
|
@@ -707,13 +758,7 @@ class AblyService extends EventEmitter {
|
|
|
707
758
|
this.emit('log', 'Warning: session channel not available');
|
|
708
759
|
return;
|
|
709
760
|
}
|
|
710
|
-
|
|
711
|
-
try {
|
|
712
|
-
this.emit('log', `Publishing response: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
713
|
-
await this._sessionChannel.publish('response', message);
|
|
714
|
-
} catch (err) {
|
|
715
|
-
this.emit('log', `Failed to publish response: ${err.message}`);
|
|
716
|
-
}
|
|
761
|
+
await this._enqueuePublish('response', message);
|
|
717
762
|
}
|
|
718
763
|
|
|
719
764
|
/**
|
|
@@ -721,13 +766,7 @@ class AblyService extends EventEmitter {
|
|
|
721
766
|
*/
|
|
722
767
|
async sendFile(message) {
|
|
723
768
|
if (!this._sessionChannel) return;
|
|
724
|
-
|
|
725
|
-
try {
|
|
726
|
-
this.emit('log', `Publishing file: type=${message.type || 'unknown'} (requestId=${message.requestId || 'none'})`);
|
|
727
|
-
await this._sessionChannel.publish('file', message);
|
|
728
|
-
} catch (err) {
|
|
729
|
-
this.emit('log', `Failed to publish file: ${err.message}`);
|
|
730
|
-
}
|
|
769
|
+
await this._enqueuePublish('file', message);
|
|
731
770
|
}
|
|
732
771
|
|
|
733
772
|
/**
|
|
@@ -735,13 +774,7 @@ class AblyService extends EventEmitter {
|
|
|
735
774
|
*/
|
|
736
775
|
async sendControl(message) {
|
|
737
776
|
if (!this._sessionChannel) return;
|
|
738
|
-
|
|
739
|
-
try {
|
|
740
|
-
this.emit('log', `Publishing control: type=${message.type || 'unknown'}`);
|
|
741
|
-
await this._sessionChannel.publish('control', message);
|
|
742
|
-
} catch (err) {
|
|
743
|
-
this.emit('log', `Failed to publish control: ${err.message}`);
|
|
744
|
-
}
|
|
777
|
+
await this._enqueuePublish('control', message);
|
|
745
778
|
}
|
|
746
779
|
|
|
747
780
|
/**
|
|
@@ -767,6 +800,14 @@ class AblyService extends EventEmitter {
|
|
|
767
800
|
this._statsInterval = null;
|
|
768
801
|
}
|
|
769
802
|
|
|
803
|
+
if (this._logFlushTimer) {
|
|
804
|
+
clearTimeout(this._logFlushTimer);
|
|
805
|
+
this._logFlushTimer = null;
|
|
806
|
+
}
|
|
807
|
+
this._pendingLogs = [];
|
|
808
|
+
for (const item of this._publishQueue) item.resolve();
|
|
809
|
+
this._publishQueue.length = 0;
|
|
810
|
+
|
|
770
811
|
// Null out subscription refs so _recoverFromDiscontinuity won't replay
|
|
771
812
|
// messages on a channel that is about to be torn down
|
|
772
813
|
this._commandSubscription = null;
|
package/lib/automation.js
CHANGED
|
@@ -37,6 +37,20 @@ if (IS_LINUX && !process.env.DISPLAY) {
|
|
|
37
37
|
const API_ROOT = process.env.TD_API_ROOT || process.env.API_ROOT || 'https://api.testdriver.ai';
|
|
38
38
|
const API_KEY = process.env.TD_API_KEY;
|
|
39
39
|
|
|
40
|
+
// ─── Exec output limits ──────────────────────────────────────────────────────
|
|
41
|
+
// Flush size keeps each streamed message well under Ably's 64KB hard limit
|
|
42
|
+
// (leaves room for the JSON envelope + string escaping). The total cap stops a
|
|
43
|
+
// runaway command (`tail -f`, an infinite loop) from streaming forever and
|
|
44
|
+
// starving the session channel's shared publish budget.
|
|
45
|
+
const EXEC_CHUNK_FLUSH_SIZE = 32 * 1024;
|
|
46
|
+
const EXEC_OUTPUT_MAX_BYTES = Number(process.env.TD_EXEC_MAX_OUTPUT_BYTES) || 5 * 1024 * 1024;
|
|
47
|
+
const EXEC_STDERR_MAX_BYTES = 256 * 1024;
|
|
48
|
+
|
|
49
|
+
function truncateText(text, maxBytes) {
|
|
50
|
+
if (typeof text !== 'string' || text.length <= maxBytes) return text;
|
|
51
|
+
return text.slice(0, maxBytes) + `\n[truncated — exceeded ${maxBytes} bytes]`;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
// ─── pyautogui via Python subprocess ─────────────────────────────────────────
|
|
41
55
|
// Calls pyautogui functions via `python3 -c` (or `python -c` on Windows).
|
|
42
56
|
// Each command spawns a short-lived Python process. The pyautogui library is
|
|
@@ -422,6 +436,7 @@ class ShellSession {
|
|
|
422
436
|
constructor() {
|
|
423
437
|
this._process = null;
|
|
424
438
|
this._stderrLines = [];
|
|
439
|
+
this._stderrBytes = 0;
|
|
425
440
|
this._lock = false;
|
|
426
441
|
}
|
|
427
442
|
|
|
@@ -444,7 +459,10 @@ class ShellSession {
|
|
|
444
459
|
});
|
|
445
460
|
|
|
446
461
|
this._process.stderr.on('data', (chunk) => {
|
|
447
|
-
this.
|
|
462
|
+
if (this._stderrBytes >= EXEC_STDERR_MAX_BYTES) return;
|
|
463
|
+
const text = chunk.toString();
|
|
464
|
+
this._stderrLines.push(text);
|
|
465
|
+
this._stderrBytes += text.length;
|
|
448
466
|
});
|
|
449
467
|
|
|
450
468
|
this._process.on('error', (err) => {
|
|
@@ -484,6 +502,7 @@ class ShellSession {
|
|
|
484
502
|
|
|
485
503
|
// Clear stderr buffer
|
|
486
504
|
this._stderrLines = [];
|
|
505
|
+
this._stderrBytes = 0;
|
|
487
506
|
|
|
488
507
|
// Capture the process reference now — the exit handler may set
|
|
489
508
|
// this._process to null before the Promise resolves/times out.
|
|
@@ -514,6 +533,7 @@ class ShellSession {
|
|
|
514
533
|
}, timeoutMs);
|
|
515
534
|
|
|
516
535
|
const stdoutLines = [];
|
|
536
|
+
let stdoutBytes = 0;
|
|
517
537
|
|
|
518
538
|
const onData = (chunk) => {
|
|
519
539
|
const text = chunk.toString();
|
|
@@ -531,7 +551,12 @@ class ShellSession {
|
|
|
531
551
|
});
|
|
532
552
|
return;
|
|
533
553
|
}
|
|
534
|
-
|
|
554
|
+
// Keep the in-memory buffer bounded; a runaway command would
|
|
555
|
+
// otherwise grow it until the runner OOMs.
|
|
556
|
+
if (stdoutBytes < EXEC_OUTPUT_MAX_BYTES) {
|
|
557
|
+
stdoutLines.push(line);
|
|
558
|
+
stdoutBytes += line.length + 1;
|
|
559
|
+
}
|
|
535
560
|
}
|
|
536
561
|
|
|
537
562
|
// Stream chunk to caller if callback provided
|
|
@@ -891,15 +916,13 @@ class Automation extends EventEmitter {
|
|
|
891
916
|
const timeout = Math.ceil((data.timeout || 300000) / 1000); // ms to seconds
|
|
892
917
|
const requestId = data.requestId;
|
|
893
918
|
|
|
894
|
-
// Buffer stdout chunks
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
// string escaping while halving the message count vs the previous
|
|
898
|
-
// 16KB size, helping avoid Ably's per-channel rate limit on verbose
|
|
899
|
-
// commands. The SDK accumulates these chunks and reconstructs the
|
|
900
|
-
// full stdout — the final response only carries returncode + stderr.
|
|
901
|
-
const CHUNK_FLUSH_SIZE = 32 * 1024; // 32KB
|
|
919
|
+
// Buffer stdout chunks before emitting over Ably. The SDK accumulates
|
|
920
|
+
// these chunks and reconstructs the full stdout — the final response
|
|
921
|
+
// only carries returncode + stderr.
|
|
902
922
|
let chunkBuffer = '';
|
|
923
|
+
let streamedBytes = 0;
|
|
924
|
+
let truncated = false;
|
|
925
|
+
|
|
903
926
|
const flushChunkBuffer = () => {
|
|
904
927
|
if (chunkBuffer.length > 0) {
|
|
905
928
|
this.emit('exec.output', { requestId, chunk: chunkBuffer });
|
|
@@ -907,15 +930,34 @@ class Automation extends EventEmitter {
|
|
|
907
930
|
}
|
|
908
931
|
};
|
|
909
932
|
|
|
933
|
+
// A runaway command (tail -f, a server log, an infinite loop) would
|
|
934
|
+
// otherwise stream forever and starve the session channel. Stop
|
|
935
|
+
// forwarding past the cap and tell the SDK once that it was cut off.
|
|
936
|
+
const onChunk = (text) => {
|
|
937
|
+
if (truncated) return;
|
|
938
|
+
|
|
939
|
+
const remaining = EXEC_OUTPUT_MAX_BYTES - streamedBytes;
|
|
940
|
+
if (text.length >= remaining) {
|
|
941
|
+
chunkBuffer += text.slice(0, Math.max(remaining, 0));
|
|
942
|
+
streamedBytes = EXEC_OUTPUT_MAX_BYTES;
|
|
943
|
+
truncated = true;
|
|
944
|
+
flushChunkBuffer();
|
|
945
|
+
this.emit('exec.output', {
|
|
946
|
+
requestId,
|
|
947
|
+
chunk: `\n[output truncated — exceeded ${EXEC_OUTPUT_MAX_BYTES} bytes]\n`,
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
chunkBuffer += text;
|
|
953
|
+
streamedBytes += text.length;
|
|
954
|
+
if (chunkBuffer.length >= EXEC_CHUNK_FLUSH_SIZE) {
|
|
955
|
+
flushChunkBuffer();
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
|
|
910
959
|
try {
|
|
911
|
-
const result = await this._shell.run(data.command, timeout, {
|
|
912
|
-
onChunk: (text) => {
|
|
913
|
-
chunkBuffer += text;
|
|
914
|
-
if (chunkBuffer.length >= CHUNK_FLUSH_SIZE) {
|
|
915
|
-
flushChunkBuffer();
|
|
916
|
-
}
|
|
917
|
-
},
|
|
918
|
-
});
|
|
960
|
+
const result = await this._shell.run(data.command, timeout, { onChunk });
|
|
919
961
|
|
|
920
962
|
// Flush any remaining buffered output
|
|
921
963
|
flushChunkBuffer();
|
|
@@ -928,7 +970,7 @@ class Automation extends EventEmitter {
|
|
|
928
970
|
out: {
|
|
929
971
|
returncode: result.returncode,
|
|
930
972
|
stdout: '',
|
|
931
|
-
stderr: result.stderr,
|
|
973
|
+
stderr: truncateText(result.stderr, EXEC_STDERR_MAX_BYTES),
|
|
932
974
|
},
|
|
933
975
|
};
|
|
934
976
|
} catch (err) {
|