@fenwave/agent 1.1.0

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.
@@ -0,0 +1,247 @@
1
+ import { docker } from './containers.js';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { PassThrough } from 'stream';
4
+
5
+ async function handleTerminalAction(
6
+ ws,
7
+ clientId,
8
+ action,
9
+ payload,
10
+ terminalSessions
11
+ ) {
12
+ switch (action) {
13
+ case 'openTerminalSession':
14
+ return await handleOpenTerminalSession(ws, payload, terminalSessions);
15
+ case 'closeTerminalSession':
16
+ return await handleCloseTerminalSession(ws, payload, terminalSessions);
17
+ case 'sendTerminalInput':
18
+ return await handleSendTerminalInput(ws, payload, terminalSessions);
19
+ case 'resizeTerminal':
20
+ return await handleResizeTerminal(ws, payload, terminalSessions);
21
+ default:
22
+ throw new Error(`Unknown terminal action: ${action}`);
23
+ }
24
+ }
25
+
26
+ async function handleOpenTerminalSession(ws, payload, terminalSessions) {
27
+ try {
28
+ const { containerId, requestId } = payload;
29
+ const sessionId = `term-${uuidv4()}`;
30
+
31
+ const container = docker.getContainer(containerId);
32
+ const exec = await container.exec({
33
+ AttachStdin: true,
34
+ AttachStdout: true,
35
+ AttachStderr: true,
36
+ Tty: true,
37
+ Cmd: ['/bin/sh'],
38
+ });
39
+
40
+ const stream = await exec.start({
41
+ hijack: true,
42
+ stdin: true,
43
+ });
44
+
45
+ // Store the session with the exec instance
46
+ terminalSessions.set(sessionId, {
47
+ stream,
48
+ exec,
49
+ containerId,
50
+ lastActivity: Date.now(),
51
+ });
52
+
53
+ // Use docker.modem.demuxStream to properly handle the stream
54
+ const stdout = new PassThrough();
55
+ const stderr = new PassThrough();
56
+
57
+ // Handle stdout data
58
+ stdout.on('data', (chunk) => {
59
+ ws.send(
60
+ JSON.stringify({
61
+ type: 'terminalOutput',
62
+ sessionId,
63
+ output: chunk.toString(),
64
+ })
65
+ );
66
+ });
67
+
68
+ // Handle stderr data
69
+ stderr.on('data', (chunk) => {
70
+ ws.send(
71
+ JSON.stringify({
72
+ type: 'terminalOutput',
73
+ sessionId,
74
+ output: chunk.toString(),
75
+ })
76
+ );
77
+ });
78
+
79
+ // Demux the stream to remove Docker's multiplexing headers
80
+ docker.modem.demuxStream(stream, stdout, stderr);
81
+
82
+ // Handle stream end
83
+ stream.on('end', () => {
84
+ terminalSessions.delete(sessionId);
85
+ ws.send(
86
+ JSON.stringify({
87
+ type: 'terminalEnded',
88
+ sessionId,
89
+ })
90
+ );
91
+ });
92
+
93
+ // Handle stream error
94
+ stream.on('error', (error) => {
95
+ console.error('Terminal stream error:', error);
96
+ terminalSessions.delete(sessionId);
97
+ ws.send(
98
+ JSON.stringify({
99
+ type: 'error',
100
+ error: 'Terminal error: ' + error.message,
101
+ sessionId,
102
+ })
103
+ );
104
+ });
105
+
106
+ ws.send(
107
+ JSON.stringify({
108
+ type: 'terminalOpened',
109
+ sessionId,
110
+ containerId,
111
+ requestId,
112
+ })
113
+ );
114
+ } catch (error) {
115
+ console.error('Error opening terminal session:', error);
116
+ ws.send(
117
+ JSON.stringify({
118
+ type: 'error',
119
+ error: 'Failed to open terminal session: ' + error.message,
120
+ requestId: payload.requestId,
121
+ })
122
+ );
123
+ }
124
+ }
125
+
126
+ async function handleCloseTerminalSession(ws, payload, terminalSessions) {
127
+ try {
128
+ const { sessionId, requestId } = payload;
129
+
130
+ if (terminalSessions.has(sessionId)) {
131
+ const session = terminalSessions.get(sessionId);
132
+ if (session.stream.destroy) {
133
+ session.stream.destroy();
134
+ }
135
+ terminalSessions.delete(sessionId);
136
+ }
137
+
138
+ ws.send(
139
+ JSON.stringify({
140
+ type: 'terminalClosed',
141
+ sessionId,
142
+ success: true,
143
+ requestId,
144
+ })
145
+ );
146
+ } catch (error) {
147
+ console.error('Error closing terminal session:', error);
148
+ ws.send(
149
+ JSON.stringify({
150
+ type: 'error',
151
+ error: 'Failed to close terminal session: ' + error.message,
152
+ requestId: payload.requestId,
153
+ })
154
+ );
155
+ }
156
+ }
157
+
158
+ async function handleSendTerminalInput(ws, payload, terminalSessions) {
159
+ try {
160
+ const { sessionId, input, requestId } = payload;
161
+
162
+ if (!terminalSessions.has(sessionId)) {
163
+ throw new Error('Terminal session not found');
164
+ }
165
+
166
+ const session = terminalSessions.get(sessionId);
167
+ session.lastActivity = Date.now();
168
+ session.stream.write(input);
169
+
170
+ ws.send(
171
+ JSON.stringify({
172
+ type: 'terminalInputSent',
173
+ sessionId,
174
+ success: true,
175
+ requestId,
176
+ })
177
+ );
178
+ } catch (error) {
179
+ console.error('Error sending terminal input:', error);
180
+ ws.send(
181
+ JSON.stringify({
182
+ type: 'error',
183
+ error: 'Failed to send terminal input: ' + error.message,
184
+ requestId: payload.requestId,
185
+ })
186
+ );
187
+ }
188
+ }
189
+
190
+ async function handleResizeTerminal(ws, payload, terminalSessions) {
191
+ try {
192
+ const { sessionId, cols, rows, requestId } = payload;
193
+
194
+ if (!terminalSessions.has(sessionId)) {
195
+ throw new Error('Terminal session not found');
196
+ }
197
+
198
+ const session = terminalSessions.get(sessionId);
199
+ session.lastActivity = Date.now();
200
+
201
+ // Resize the terminal if the exec instance supports it
202
+ if (session.exec && typeof session.exec.resize === 'function') {
203
+ await session.exec.resize({ h: rows, w: cols });
204
+ }
205
+
206
+ ws.send(
207
+ JSON.stringify({
208
+ type: 'terminalResized',
209
+ sessionId,
210
+ success: true,
211
+ requestId,
212
+ })
213
+ );
214
+ } catch (error) {
215
+ console.error('Error resizing terminal:', error);
216
+ ws.send(
217
+ JSON.stringify({
218
+ type: 'error',
219
+ error: 'Failed to resize terminal: ' + error.message,
220
+ requestId: payload.requestId,
221
+ })
222
+ );
223
+ }
224
+ }
225
+
226
+ // Add a cleanup function for terminal sessions
227
+ function cleanupInactiveTerminalSessions(terminalSessions) {
228
+ const now = Date.now();
229
+ const inactivityThreshold = 15 * 60 * 1000; // 15 minutes
230
+
231
+ for (const [sessionId, session] of terminalSessions.entries()) {
232
+ if (now - session.lastActivity > inactivityThreshold) {
233
+ console.log(`Cleaning up inactive terminal session: ${sessionId}`);
234
+ if (session.stream.destroy) {
235
+ session.stream.destroy();
236
+ }
237
+ terminalSessions.delete(sessionId);
238
+ }
239
+ }
240
+ }
241
+
242
+ export default { handleTerminalAction, cleanupInactiveTerminalSessions };
243
+
244
+ export {
245
+ handleTerminalAction,
246
+ cleanupInactiveTerminalSessions,
247
+ };