shell-mirror 1.5.44 โ 1.5.46
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/mac-agent/agent.js +171 -11
- package/package.json +1 -1
- package/public/app/terminal.html +1 -1
- package/public/app/terminal.js +109 -0
package/mac-agent/agent.js
CHANGED
|
@@ -55,17 +55,79 @@ logToFile(`๐ Shell: ${shell}`);
|
|
|
55
55
|
|
|
56
56
|
// Circular buffer for session output persistence
|
|
57
57
|
class CircularBuffer {
|
|
58
|
-
constructor(size = 10000) {
|
|
58
|
+
constructor(size = 10000, maxTotalSize = 512 * 1024) { // 512KB max total size
|
|
59
59
|
this.size = size;
|
|
60
|
+
this.maxTotalSize = maxTotalSize;
|
|
60
61
|
this.buffer = [];
|
|
61
62
|
this.index = 0;
|
|
62
63
|
this.full = false;
|
|
64
|
+
this.totalSize = 0;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
add(data) {
|
|
68
|
+
const dataSize = Buffer.byteLength(data, 'utf8');
|
|
69
|
+
|
|
70
|
+
// Add new data
|
|
71
|
+
const oldData = this.buffer[this.index];
|
|
72
|
+
if (oldData) {
|
|
73
|
+
this.totalSize -= Buffer.byteLength(oldData, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
this.buffer[this.index] = data;
|
|
77
|
+
this.totalSize += dataSize;
|
|
67
78
|
this.index = (this.index + 1) % this.size;
|
|
68
79
|
if (this.index === 0) this.full = true;
|
|
80
|
+
|
|
81
|
+
// If total size exceeds limit, remove older data
|
|
82
|
+
this.enforceMaxSize();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
enforceMaxSize() {
|
|
86
|
+
if (this.totalSize <= this.maxTotalSize) return;
|
|
87
|
+
|
|
88
|
+
// Remove data from the oldest end until under limit
|
|
89
|
+
let removed = 0;
|
|
90
|
+
while (this.totalSize > this.maxTotalSize && this.getTotalItems() > 0) {
|
|
91
|
+
let oldestIndex;
|
|
92
|
+
if (this.full) {
|
|
93
|
+
oldestIndex = this.index; // Oldest item when buffer is full
|
|
94
|
+
} else {
|
|
95
|
+
oldestIndex = 0; // Start from beginning when not full
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const oldestData = this.buffer[oldestIndex];
|
|
99
|
+
if (oldestData) {
|
|
100
|
+
this.totalSize -= Buffer.byteLength(oldestData, 'utf8');
|
|
101
|
+
this.buffer[oldestIndex] = '';
|
|
102
|
+
removed++;
|
|
103
|
+
|
|
104
|
+
if (this.full) {
|
|
105
|
+
this.index = (this.index + 1) % this.size;
|
|
106
|
+
if (this.getTotalItems() === 0) {
|
|
107
|
+
this.full = false;
|
|
108
|
+
this.index = 0;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// Shift array to remove empty spot
|
|
112
|
+
this.buffer.splice(oldestIndex, 1);
|
|
113
|
+
this.buffer.push('');
|
|
114
|
+
this.index = Math.max(0, this.index - 1);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
break; // Prevent infinite loop
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (removed > 0) {
|
|
122
|
+
logToFile(`[BUFFER] Enforced max size limit: removed ${removed} old entries, total size now ${this.totalSize} bytes`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getTotalItems() {
|
|
127
|
+
if (!this.full) {
|
|
128
|
+
return this.buffer.slice(0, this.index).filter(item => item).length;
|
|
129
|
+
}
|
|
130
|
+
return this.buffer.filter(item => item).length;
|
|
69
131
|
}
|
|
70
132
|
|
|
71
133
|
getAll() {
|
|
@@ -79,6 +141,16 @@ class CircularBuffer {
|
|
|
79
141
|
this.buffer = [];
|
|
80
142
|
this.index = 0;
|
|
81
143
|
this.full = false;
|
|
144
|
+
this.totalSize = 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getStats() {
|
|
148
|
+
return {
|
|
149
|
+
items: this.getTotalItems(),
|
|
150
|
+
totalSize: this.totalSize,
|
|
151
|
+
maxSize: this.maxTotalSize,
|
|
152
|
+
utilizationPercent: Math.round((this.totalSize / this.maxTotalSize) * 100)
|
|
153
|
+
};
|
|
82
154
|
}
|
|
83
155
|
}
|
|
84
156
|
|
|
@@ -200,12 +272,7 @@ class SessionManager {
|
|
|
200
272
|
session.lastActivity = Date.now();
|
|
201
273
|
|
|
202
274
|
logToFile(`[SESSION] โ
Client ${clientId} connected to session ${sessionId}`);
|
|
203
|
-
|
|
204
|
-
// Send buffered output to newly connected client
|
|
205
|
-
const bufferedOutput = session.buffer.getAll();
|
|
206
|
-
if (bufferedOutput) {
|
|
207
|
-
this.sendToClient(clientId, { type: 'output', data: bufferedOutput });
|
|
208
|
-
}
|
|
275
|
+
logToFile(`[SESSION] โน๏ธ Buffered output will be sent when WebRTC data channel opens`);
|
|
209
276
|
|
|
210
277
|
return true;
|
|
211
278
|
}
|
|
@@ -266,10 +333,9 @@ class SessionManager {
|
|
|
266
333
|
// For now, we'll use a global dataChannel reference
|
|
267
334
|
// In a full implementation, this would use a clientId-to-dataChannel mapping
|
|
268
335
|
if (typeof dataChannel !== 'undefined' && dataChannel && dataChannel.readyState === 'open') {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
logToFile(`[SESSION] Error sending to client ${clientId}: ${err.message}`);
|
|
336
|
+
const success = sendLargeMessage(dataChannel, message, '[SESSION]');
|
|
337
|
+
if (!success) {
|
|
338
|
+
logToFile(`[SESSION] โ Failed to send message to client ${clientId}`);
|
|
273
339
|
}
|
|
274
340
|
} else {
|
|
275
341
|
logToFile(`[SESSION] โ ๏ธ Cannot send to client ${clientId} - data channel not available`);
|
|
@@ -428,6 +494,11 @@ function connectToSignalingServer() {
|
|
|
428
494
|
// Force ICE gathering if it hasn't started within 2 seconds
|
|
429
495
|
logToFile('[AGENT] ๐ง Setting up ICE gathering fallback timer...');
|
|
430
496
|
setTimeout(() => {
|
|
497
|
+
if (!peerConnection) {
|
|
498
|
+
logToFile('[AGENT] โ ๏ธ ICE gathering timer fired but peerConnection is null (connection already closed)');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
431
502
|
if (peerConnection.iceGatheringState === 'new') {
|
|
432
503
|
logToFile('[AGENT] โ ๏ธ ICE gathering hasn\'t started - checking peer connection state');
|
|
433
504
|
logToFile(`[AGENT] Current ICE gathering state: ${peerConnection.iceGatheringState}`);
|
|
@@ -633,9 +704,98 @@ function cleanup(clientId = null) {
|
|
|
633
704
|
}
|
|
634
705
|
}
|
|
635
706
|
|
|
707
|
+
// WebRTC data channel message size limits and chunking
|
|
708
|
+
const MAX_WEBRTC_MESSAGE_SIZE = 32 * 1024; // 32KB - conservative limit for compatibility
|
|
709
|
+
const CHUNK_TYPE_START = 'chunk_start';
|
|
710
|
+
const CHUNK_TYPE_DATA = 'chunk_data';
|
|
711
|
+
const CHUNK_TYPE_END = 'chunk_end';
|
|
712
|
+
|
|
713
|
+
function sendLargeMessage(dataChannel, message, logPrefix = '[AGENT]') {
|
|
714
|
+
try {
|
|
715
|
+
const messageStr = JSON.stringify(message);
|
|
716
|
+
const messageBytes = Buffer.byteLength(messageStr, 'utf8');
|
|
717
|
+
|
|
718
|
+
if (messageBytes <= MAX_WEBRTC_MESSAGE_SIZE) {
|
|
719
|
+
// Small message, send directly
|
|
720
|
+
dataChannel.send(messageStr);
|
|
721
|
+
logToFile(`${logPrefix} โ
Sent small message (${messageBytes} bytes)`);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Large message, chunk it
|
|
726
|
+
logToFile(`${logPrefix} ๐ฆ Chunking large message (${messageBytes} bytes) into ${MAX_WEBRTC_MESSAGE_SIZE} byte chunks`);
|
|
727
|
+
|
|
728
|
+
const chunkId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
|
729
|
+
const chunks = [];
|
|
730
|
+
|
|
731
|
+
// Split message string into chunks
|
|
732
|
+
for (let i = 0; i < messageStr.length; i += MAX_WEBRTC_MESSAGE_SIZE - 200) { // Reserve 200 bytes for chunk metadata
|
|
733
|
+
chunks.push(messageStr.slice(i, i + MAX_WEBRTC_MESSAGE_SIZE - 200));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
logToFile(`${logPrefix} ๐ฆ Split into ${chunks.length} chunks`);
|
|
737
|
+
|
|
738
|
+
// Send chunk start notification
|
|
739
|
+
dataChannel.send(JSON.stringify({
|
|
740
|
+
type: CHUNK_TYPE_START,
|
|
741
|
+
chunkId: chunkId,
|
|
742
|
+
totalChunks: chunks.length,
|
|
743
|
+
totalSize: messageBytes,
|
|
744
|
+
originalType: message.type
|
|
745
|
+
}));
|
|
746
|
+
|
|
747
|
+
// Send each chunk with a small delay to prevent overwhelming
|
|
748
|
+
chunks.forEach((chunk, index) => {
|
|
749
|
+
setTimeout(() => {
|
|
750
|
+
try {
|
|
751
|
+
dataChannel.send(JSON.stringify({
|
|
752
|
+
type: CHUNK_TYPE_DATA,
|
|
753
|
+
chunkId: chunkId,
|
|
754
|
+
chunkIndex: index,
|
|
755
|
+
data: chunk
|
|
756
|
+
}));
|
|
757
|
+
|
|
758
|
+
// Send end notification after last chunk
|
|
759
|
+
if (index === chunks.length - 1) {
|
|
760
|
+
setTimeout(() => {
|
|
761
|
+
dataChannel.send(JSON.stringify({
|
|
762
|
+
type: CHUNK_TYPE_END,
|
|
763
|
+
chunkId: chunkId
|
|
764
|
+
}));
|
|
765
|
+
logToFile(`${logPrefix} โ
Large message sent successfully (${chunks.length} chunks)`);
|
|
766
|
+
}, 10);
|
|
767
|
+
}
|
|
768
|
+
} catch (err) {
|
|
769
|
+
logToFile(`${logPrefix} โ Error sending chunk ${index}: ${err.message}`);
|
|
770
|
+
}
|
|
771
|
+
}, index * 10); // 10ms delay between chunks
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return true;
|
|
775
|
+
} catch (err) {
|
|
776
|
+
logToFile(`${logPrefix} โ Error in sendLargeMessage: ${err.message}`);
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
636
781
|
function setupDataChannel(clientId) {
|
|
637
782
|
dataChannel.onopen = () => {
|
|
638
783
|
logToFile('[AGENT] โ
Data channel is open!');
|
|
784
|
+
|
|
785
|
+
// Send buffered output for existing session when data channel opens
|
|
786
|
+
const session = sessionManager.getClientSession(clientId);
|
|
787
|
+
if (session) {
|
|
788
|
+
const bufferedOutput = session.buffer.getAll();
|
|
789
|
+
if (bufferedOutput) {
|
|
790
|
+
logToFile(`[AGENT] ๐ค Sending buffered output to client (${bufferedOutput.length} chars)`);
|
|
791
|
+
const success = sendLargeMessage(dataChannel, { type: 'output', data: bufferedOutput }, '[AGENT]');
|
|
792
|
+
if (!success) {
|
|
793
|
+
logToFile('[AGENT] โ Failed to send buffered output');
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
logToFile('[AGENT] โน๏ธ No buffered output to send for this session');
|
|
797
|
+
}
|
|
798
|
+
}
|
|
639
799
|
};
|
|
640
800
|
|
|
641
801
|
dataChannel.onmessage = (event) => {
|
package/package.json
CHANGED
package/public/app/terminal.html
CHANGED
package/public/app/terminal.js
CHANGED
|
@@ -55,6 +55,102 @@ let currentSession = null;
|
|
|
55
55
|
let availableSessions = [];
|
|
56
56
|
let requestedSessionId = null; // For connecting to specific session from URL
|
|
57
57
|
|
|
58
|
+
// Chunk reassembly for large messages
|
|
59
|
+
const chunkAssembler = {
|
|
60
|
+
activeChunks: new Map(),
|
|
61
|
+
|
|
62
|
+
handleChunkedMessage(message) {
|
|
63
|
+
const { type, chunkId } = message;
|
|
64
|
+
|
|
65
|
+
switch (type) {
|
|
66
|
+
case 'chunk_start':
|
|
67
|
+
console.log(`[CLIENT] ๐ฆ Starting chunk reassembly: ${chunkId} (${message.totalChunks} chunks, ${message.totalSize} bytes)`);
|
|
68
|
+
this.activeChunks.set(chunkId, {
|
|
69
|
+
originalType: message.originalType,
|
|
70
|
+
totalChunks: message.totalChunks,
|
|
71
|
+
totalSize: message.totalSize,
|
|
72
|
+
receivedChunks: new Map(),
|
|
73
|
+
startTime: Date.now()
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
|
|
77
|
+
case 'chunk_data':
|
|
78
|
+
const chunkInfo = this.activeChunks.get(chunkId);
|
|
79
|
+
if (!chunkInfo) {
|
|
80
|
+
console.error(`[CLIENT] โ Received chunk data for unknown chunk ID: ${chunkId}`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
chunkInfo.receivedChunks.set(message.chunkIndex, message.data);
|
|
85
|
+
console.log(`[CLIENT] ๐ฆ Received chunk ${message.chunkIndex + 1}/${chunkInfo.totalChunks}`);
|
|
86
|
+
return true;
|
|
87
|
+
|
|
88
|
+
case 'chunk_end':
|
|
89
|
+
return this.reassembleChunks(chunkId);
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
return false; // Not a chunk message
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
reassembleChunks(chunkId) {
|
|
97
|
+
const chunkInfo = this.activeChunks.get(chunkId);
|
|
98
|
+
if (!chunkInfo) {
|
|
99
|
+
console.error(`[CLIENT] โ Cannot reassemble unknown chunk: ${chunkId}`);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Check if we have all chunks
|
|
105
|
+
if (chunkInfo.receivedChunks.size !== chunkInfo.totalChunks) {
|
|
106
|
+
console.error(`[CLIENT] โ Missing chunks: expected ${chunkInfo.totalChunks}, got ${chunkInfo.receivedChunks.size}`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reassemble chunks in order
|
|
111
|
+
let reassembledData = '';
|
|
112
|
+
for (let i = 0; i < chunkInfo.totalChunks; i++) {
|
|
113
|
+
if (!chunkInfo.receivedChunks.has(i)) {
|
|
114
|
+
console.error(`[CLIENT] โ Missing chunk ${i}`);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
reassembledData += chunkInfo.receivedChunks.get(i);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const elapsed = Date.now() - chunkInfo.startTime;
|
|
121
|
+
console.log(`[CLIENT] โ
Reassembled ${chunkInfo.totalChunks} chunks in ${elapsed}ms (${reassembledData.length} chars)`);
|
|
122
|
+
|
|
123
|
+
// Parse and process the reassembled message
|
|
124
|
+
const originalMessage = JSON.parse(reassembledData);
|
|
125
|
+
this.activeChunks.delete(chunkId);
|
|
126
|
+
|
|
127
|
+
// Process the original message
|
|
128
|
+
if (originalMessage.type === 'output') {
|
|
129
|
+
term.write(originalMessage.data);
|
|
130
|
+
} else {
|
|
131
|
+
handleSessionMessage(originalMessage);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(`[CLIENT] โ Error reassembling chunks for ${chunkId}:`, err);
|
|
137
|
+
this.activeChunks.delete(chunkId);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
cleanup() {
|
|
143
|
+
// Clean up old incomplete chunks (older than 30 seconds)
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
for (const [chunkId, chunkInfo] of this.activeChunks.entries()) {
|
|
146
|
+
if (now - chunkInfo.startTime > 30000) {
|
|
147
|
+
console.log(`[CLIENT] ๐งน Cleaning up stale chunk: ${chunkId}`);
|
|
148
|
+
this.activeChunks.delete(chunkId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
58
154
|
// Connection status management
|
|
59
155
|
function updateConnectionStatus(status) {
|
|
60
156
|
const statusElement = document.getElementById('connection-status');
|
|
@@ -75,6 +171,11 @@ function updateConnectionStatus(status) {
|
|
|
75
171
|
}
|
|
76
172
|
}
|
|
77
173
|
|
|
174
|
+
// Cleanup timer for chunk assembler
|
|
175
|
+
setInterval(() => {
|
|
176
|
+
chunkAssembler.cleanup();
|
|
177
|
+
}, 30000); // Clean up every 30 seconds
|
|
178
|
+
|
|
78
179
|
// Check for agent parameter and connect directly
|
|
79
180
|
window.addEventListener('load', () => {
|
|
80
181
|
loadVersionInfo();
|
|
@@ -508,6 +609,14 @@ function setupDataChannel() {
|
|
|
508
609
|
dataChannel.onmessage = (event) => {
|
|
509
610
|
try {
|
|
510
611
|
const message = JSON.parse(event.data);
|
|
612
|
+
|
|
613
|
+
// Check if this is a chunked message
|
|
614
|
+
if (chunkAssembler.handleChunkedMessage(message)) {
|
|
615
|
+
// Message was handled by chunk assembler
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Handle normal messages
|
|
511
620
|
if (message.type === 'output') {
|
|
512
621
|
term.write(message.data);
|
|
513
622
|
} else {
|