shell-mirror 1.5.124 → 1.5.126
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 +71 -7
- package/package.json +1 -1
- package/public/app/terminal.js +83 -5
package/mac-agent/agent.js
CHANGED
|
@@ -807,15 +807,79 @@ async function createPeerConnection(clientId) {
|
|
|
807
807
|
};
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
-
function cleanup(clientId = null) {
|
|
810
|
+
async function cleanup(clientId = null) {
|
|
811
811
|
// Disconnect client from session manager
|
|
812
812
|
if (clientId) {
|
|
813
813
|
sessionManager.disconnectClient(clientId);
|
|
814
814
|
} else {
|
|
815
|
-
// Full agent shutdown -
|
|
815
|
+
// Full agent shutdown - send final heartbeat with empty sessions BEFORE stopping heartbeat
|
|
816
|
+
logToFile('[AGENT] Sending final heartbeat with empty sessions before shutdown...');
|
|
817
|
+
|
|
818
|
+
if (heartbeatInterval) {
|
|
819
|
+
const finalHeartbeatData = JSON.stringify({
|
|
820
|
+
agentId: AGENT_ID,
|
|
821
|
+
timestamp: Date.now(),
|
|
822
|
+
activeSessions: 0,
|
|
823
|
+
sessions: [], // Empty session list to clear dashboard
|
|
824
|
+
localPort: process.env.LOCAL_PORT || 8080,
|
|
825
|
+
capabilities: ['webrtc', 'direct_websocket'],
|
|
826
|
+
status: 'shutting_down'
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const options = {
|
|
830
|
+
hostname: 'shellmirror.app',
|
|
831
|
+
port: 443,
|
|
832
|
+
path: '/php-backend/api/agent-heartbeat.php',
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: {
|
|
835
|
+
'Content-Type': 'application/json',
|
|
836
|
+
'Content-Length': Buffer.byteLength(finalHeartbeatData),
|
|
837
|
+
'X-Agent-Secret': 'mac-agent-secret-2024',
|
|
838
|
+
'X-Agent-ID': AGENT_ID
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
await new Promise((resolve, reject) => {
|
|
844
|
+
const req = https.request(options, (res) => {
|
|
845
|
+
let responseData = '';
|
|
846
|
+
res.on('data', (chunk) => {
|
|
847
|
+
responseData += chunk;
|
|
848
|
+
});
|
|
849
|
+
res.on('end', () => {
|
|
850
|
+
if (res.statusCode === 200) {
|
|
851
|
+
logToFile('[AGENT] ✅ Sent final heartbeat with empty sessions');
|
|
852
|
+
resolve();
|
|
853
|
+
} else {
|
|
854
|
+
logToFile(`[AGENT] ⚠️ Final heartbeat HTTP error: ${res.statusCode}`);
|
|
855
|
+
resolve(); // Continue shutdown even if heartbeat fails
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
req.on('error', (error) => {
|
|
861
|
+
logToFile(`[AGENT] ❌ Failed to send final heartbeat: ${error.message}`);
|
|
862
|
+
resolve(); // Continue shutdown even if heartbeat fails
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
req.setTimeout(5000, () => {
|
|
866
|
+
req.destroy();
|
|
867
|
+
logToFile('[AGENT] ⚠️ Final heartbeat timed out after 5s');
|
|
868
|
+
resolve(); // Continue shutdown even if heartbeat times out
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
req.write(finalHeartbeatData);
|
|
872
|
+
req.end();
|
|
873
|
+
});
|
|
874
|
+
} catch (error) {
|
|
875
|
+
logToFile(`[AGENT] ❌ Error sending final heartbeat: ${error.message}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Now stop heartbeat system
|
|
816
880
|
stopHeartbeatSystem();
|
|
817
881
|
}
|
|
818
|
-
|
|
882
|
+
|
|
819
883
|
if (dataChannel) {
|
|
820
884
|
dataChannel.close();
|
|
821
885
|
dataChannel = null;
|
|
@@ -1058,17 +1122,17 @@ function sendMessage(message) {
|
|
|
1058
1122
|
}
|
|
1059
1123
|
|
|
1060
1124
|
// Graceful shutdown handling
|
|
1061
|
-
process.on('SIGINT', () => {
|
|
1125
|
+
process.on('SIGINT', async () => {
|
|
1062
1126
|
console.log('\n[AGENT] Shutting down gracefully...');
|
|
1063
|
-
cleanup();
|
|
1127
|
+
await cleanup();
|
|
1064
1128
|
if (ws) ws.close();
|
|
1065
1129
|
if (localServer) localServer.close();
|
|
1066
1130
|
process.exit(0);
|
|
1067
1131
|
});
|
|
1068
1132
|
|
|
1069
|
-
process.on('SIGTERM', () => {
|
|
1133
|
+
process.on('SIGTERM', async () => {
|
|
1070
1134
|
console.log('\n[AGENT] Received SIGTERM, shutting down...');
|
|
1071
|
-
cleanup();
|
|
1135
|
+
await cleanup();
|
|
1072
1136
|
if (ws) ws.close();
|
|
1073
1137
|
if (localServer) localServer.close();
|
|
1074
1138
|
process.exit(0);
|
package/package.json
CHANGED
package/public/app/terminal.js
CHANGED
|
@@ -56,6 +56,10 @@ let currentSession = null;
|
|
|
56
56
|
let availableSessions = [];
|
|
57
57
|
let requestedSessionId = null; // For connecting to specific session from URL
|
|
58
58
|
|
|
59
|
+
// Connection status messaging
|
|
60
|
+
let connectionStatusMessage = 'Connecting to agent...';
|
|
61
|
+
let connectionTimeoutWarning = null;
|
|
62
|
+
|
|
59
63
|
// Chunk reassembly for large messages
|
|
60
64
|
const chunkAssembler = {
|
|
61
65
|
activeChunks: new Map(),
|
|
@@ -166,6 +170,31 @@ function updateConnectionStatus(status) {
|
|
|
166
170
|
// else: disconnected (default red)
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// Set connection status message (shown in tab bar when no sessions)
|
|
174
|
+
function setConnectionMessage(message, writeToTerminal = true) {
|
|
175
|
+
connectionStatusMessage = message;
|
|
176
|
+
console.log('[CLIENT] 📢 Connection message:', message);
|
|
177
|
+
|
|
178
|
+
// Update the tab bar display
|
|
179
|
+
updateSessionDisplay();
|
|
180
|
+
|
|
181
|
+
// Optionally write to terminal for visibility
|
|
182
|
+
if (writeToTerminal && term) {
|
|
183
|
+
term.write(`\r\n\x1b[36m${message}\x1b[0m\r\n`); // Cyan color
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Clear connection timeout warnings (called when connection succeeds)
|
|
188
|
+
function clearConnectionTimeouts() {
|
|
189
|
+
if (connectionTimeoutWarning) {
|
|
190
|
+
clearTimeout(connectionTimeoutWarning.timeout10s);
|
|
191
|
+
clearTimeout(connectionTimeoutWarning.timeout30s);
|
|
192
|
+
clearTimeout(connectionTimeoutWarning.timeout60s);
|
|
193
|
+
connectionTimeoutWarning = null;
|
|
194
|
+
console.log('[CLIENT] ✅ Connection timeout warnings cleared');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
169
198
|
// Cleanup timer for chunk assembler
|
|
170
199
|
setInterval(() => {
|
|
171
200
|
chunkAssembler.cleanup();
|
|
@@ -244,6 +273,9 @@ function startConnection() {
|
|
|
244
273
|
terminalContainer.classList.add('show');
|
|
245
274
|
term.open(document.getElementById('terminal'));
|
|
246
275
|
|
|
276
|
+
// Show initial connection message
|
|
277
|
+
setConnectionMessage('🔗 Connecting to agent...', true);
|
|
278
|
+
|
|
247
279
|
// Initialize session display (shows header with connection status even before session exists)
|
|
248
280
|
updateSessionDisplay();
|
|
249
281
|
|
|
@@ -262,6 +294,40 @@ function startConnection() {
|
|
|
262
294
|
fitAddon.fit();
|
|
263
295
|
term.focus(); // Ensure cursor is visible even before connection
|
|
264
296
|
}, 100);
|
|
297
|
+
|
|
298
|
+
// Set up connection timeout warnings
|
|
299
|
+
const timeout10s = setTimeout(() => {
|
|
300
|
+
if (!currentSession) {
|
|
301
|
+
setConnectionMessage('⏱️ Taking longer than usual... Please wait', true);
|
|
302
|
+
term.write('\r\n\x1b[33m⏱️ Connection is taking longer than expected...\x1b[0m\r\n');
|
|
303
|
+
}
|
|
304
|
+
}, 10000);
|
|
305
|
+
|
|
306
|
+
const timeout30s = setTimeout(() => {
|
|
307
|
+
if (!currentSession) {
|
|
308
|
+
setConnectionMessage('⚠️ Connection very slow - Agent may be offline', true);
|
|
309
|
+
term.write('\x1b[33m⚠️ Still trying to connect. The agent may be offline or unreachable.\x1b[0m\r\n');
|
|
310
|
+
term.write('\x1b[36m💡 Tip: Check the agent status on the Dashboard\x1b[0m\r\n');
|
|
311
|
+
}
|
|
312
|
+
}, 30000);
|
|
313
|
+
|
|
314
|
+
const timeout60s = setTimeout(() => {
|
|
315
|
+
if (!currentSession) {
|
|
316
|
+
updateConnectionStatus('disconnected');
|
|
317
|
+
setConnectionMessage('❌ Failed to connect - Agent or session unavailable', false);
|
|
318
|
+
term.write('\r\n\r\n\x1b[31m❌ Connection Failed\x1b[0m\r\n');
|
|
319
|
+
term.write('\x1b[33mThe agent or session you requested is not available.\x1b[0m\r\n');
|
|
320
|
+
term.write('\r\n\x1b[36mPossible reasons:\x1b[0m\r\n');
|
|
321
|
+
term.write(' • Agent is offline or shut down\r\n');
|
|
322
|
+
term.write(' • Session was terminated\r\n');
|
|
323
|
+
term.write(' • Network connectivity issues\r\n');
|
|
324
|
+
term.write('\r\n\x1b[36m💡 Click Dashboard to return and try another session\x1b[0m\r\n');
|
|
325
|
+
}
|
|
326
|
+
}, 60000);
|
|
327
|
+
|
|
328
|
+
// Store timeout IDs so they can be cleared on successful connection
|
|
329
|
+
connectionTimeoutWarning = { timeout10s, timeout30s, timeout60s };
|
|
330
|
+
|
|
265
331
|
initialize();
|
|
266
332
|
}
|
|
267
333
|
|
|
@@ -275,17 +341,20 @@ async function initialize() {
|
|
|
275
341
|
|
|
276
342
|
if (directConnectionSuccess) {
|
|
277
343
|
console.log('[CLIENT] ✅ Direct connection established - no server needed!');
|
|
344
|
+
setConnectionMessage('✅ Connected via local network!', true);
|
|
278
345
|
return;
|
|
279
346
|
}
|
|
280
|
-
|
|
347
|
+
|
|
281
348
|
console.log('[CLIENT] ⚠️ Direct connection failed, falling back to WebRTC signaling...');
|
|
349
|
+
setConnectionMessage('🌐 Attempting WebRTC connection...', true);
|
|
282
350
|
await initializeWebRTCSignaling();
|
|
283
351
|
}
|
|
284
352
|
|
|
285
353
|
async function tryDirectConnection() {
|
|
286
354
|
console.log('[CLIENT] 🔗 Attempting direct connection to agent...');
|
|
287
355
|
updateConnectionStatus('connecting');
|
|
288
|
-
|
|
356
|
+
setConnectionMessage('🔍 Trying direct connection to local network...', true);
|
|
357
|
+
|
|
289
358
|
// Get agent data from API to find local connection details
|
|
290
359
|
try {
|
|
291
360
|
const response = await fetch('/php-backend/api/agents-list.php', {
|
|
@@ -394,6 +463,10 @@ function setupDirectConnection(directWs) {
|
|
|
394
463
|
case 'session_created':
|
|
395
464
|
console.log('[CLIENT] ✅ Direct session created:', data.sessionId);
|
|
396
465
|
|
|
466
|
+
// Clear connection timeout warnings
|
|
467
|
+
clearConnectionTimeouts();
|
|
468
|
+
setConnectionMessage('✅ Session created successfully!', false);
|
|
469
|
+
|
|
397
470
|
// Update current session
|
|
398
471
|
currentSession = {
|
|
399
472
|
id: data.sessionId,
|
|
@@ -570,6 +643,10 @@ async function initializeWebRTCSignaling() {
|
|
|
570
643
|
|
|
571
644
|
// Handle session assignment from agent
|
|
572
645
|
if (nextData.sessionId) {
|
|
646
|
+
// Clear connection timeout warnings
|
|
647
|
+
clearConnectionTimeouts();
|
|
648
|
+
setConnectionMessage('✅ Session connected!', false);
|
|
649
|
+
|
|
573
650
|
currentSession = {
|
|
574
651
|
id: nextData.sessionId,
|
|
575
652
|
name: nextData.sessionName || 'Terminal Session',
|
|
@@ -577,7 +654,7 @@ async function initializeWebRTCSignaling() {
|
|
|
577
654
|
};
|
|
578
655
|
console.log('[CLIENT] 📋 Session assigned:', currentSession);
|
|
579
656
|
console.log('[CLIENT] 🔍 Agent ID for storage:', AGENT_ID);
|
|
580
|
-
|
|
657
|
+
|
|
581
658
|
// Update UI to show session info
|
|
582
659
|
updateSessionDisplay();
|
|
583
660
|
|
|
@@ -793,6 +870,7 @@ async function createPeerConnection() {
|
|
|
793
870
|
console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
|
|
794
871
|
console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
|
|
795
872
|
updateConnectionStatus('disconnected');
|
|
873
|
+
setConnectionMessage('❌ Unable to connect - Agent may be offline', false);
|
|
796
874
|
term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
|
|
797
875
|
term.write('💡 This may be due to:\r\n');
|
|
798
876
|
term.write(' • Firewall blocking WebRTC traffic\r\n');
|
|
@@ -1043,8 +1121,8 @@ function renderTabs() {
|
|
|
1043
1121
|
// Add new session button only when we have sessions
|
|
1044
1122
|
tabsHTML += '<button class="session-tab-new" onclick="createNewSession()" title="New Session">+</button>';
|
|
1045
1123
|
} else {
|
|
1046
|
-
// No sessions
|
|
1047
|
-
tabsHTML =
|
|
1124
|
+
// No sessions - show connection status message
|
|
1125
|
+
tabsHTML = `<div style="color: #888; font-size: 0.85rem; padding: 6px 12px;">${connectionStatusMessage}</div>`;
|
|
1048
1126
|
}
|
|
1049
1127
|
|
|
1050
1128
|
tabBar.innerHTML = tabsHTML;
|