diff --git a/src/app/project/app/[appId]/overview/terminal-streamed.tsx b/src/app/project/app/[appId]/overview/terminal-streamed.tsx index ea1972e..33ebb14 100644 --- a/src/app/project/app/[appId]/overview/terminal-streamed.tsx +++ b/src/app/project/app/[appId]/overview/terminal-streamed.tsx @@ -11,6 +11,7 @@ import { Terminal } from '@xterm/xterm' import '@xterm/xterm/css/xterm.css' import { podTerminalSocket } from "@/frontend/sockets/sockets"; import { StreamUtils } from "@/shared/utils/stream.utils"; +import { Button } from "@/components/ui/button"; export default function TerminalStreamed({ terminalInfo, @@ -18,18 +19,25 @@ export default function TerminalStreamed({ terminalInfo: TerminalSetupInfoModel; }) { const [isConnected, setIsConnected] = useState(false); - const [logs, setLogs] = useState(''); const terminalWindow = useRef(null); + const [terminal, setTerminal] = useState(undefined); + const [sessionTerminalInfo, setSessionTerminalInfo] = useState(undefined); - - - useEffect(() => { + const startTerminalSession = (terminalType: 'sh' | 'bash') => { if (!terminalInfo || !terminalWindow || !terminalWindow.current) { return; } - const terminalInputKey = StreamUtils.getInputStreamName(terminalInfo); - const terminalOutputKey = StreamUtils.getOutputStreamName(terminalInfo); + const terminalSessionKey = `${terminalInfo.namespace}-${terminalInfo.podName}-${terminalInfo.containerName}-${terminalType}-${new Date().getTime()}`; + const termInfo = { + ...terminalInfo, + terminalSessionKey, + terminalType, + }; + const terminalInputKey = StreamUtils.getInputStreamName(termInfo); + const terminalOutputKey = StreamUtils.getOutputStreamName(termInfo); + console.log(`InputKey ${terminalInputKey}`); + console.log(`OutputKey ${terminalOutputKey}`); var term = new Terminal(); term.open(terminalWindow.current); @@ -41,22 +49,30 @@ export default function TerminalStreamed({ console.log('Received data:', data); term.write(data); }); - podTerminalSocket.emit('openTerminal', terminalInfo); - - + podTerminalSocket.emit('openTerminal', termInfo); term.write('Terminal is ready'); + setTerminal(term); + setSessionTerminalInfo(termInfo); + }; + useEffect(() => { return () => { console.log('Disconnecting from terminal...'); - term.dispose(); - podTerminalSocket.emit('closeTerminal', terminalInfo); + //terminal?.dispose(); + //if (sessionTerminalInfo) podTerminalSocket.emit('closeTerminal', sessionTerminalInfo); }; - }, [terminalInfo]); + }); return <>
+ {!sessionTerminalInfo && <> +
+ + +
+ }
diff --git a/src/frontend/sockets/sockets.ts b/src/frontend/sockets/sockets.ts index b0ed19e..2577bf2 100644 --- a/src/frontend/sockets/sockets.ts +++ b/src/frontend/sockets/sockets.ts @@ -3,5 +3,4 @@ import { Manager } from "socket.io-client"; const manager = new Manager(); - export const podTerminalSocket = manager.socket("/pod-terminal"); \ No newline at end of file diff --git a/src/server/services/terminal.service.ts b/src/server/services/terminal.service.ts index 1b44d3b..3e97ea4 100644 --- a/src/server/services/terminal.service.ts +++ b/src/server/services/terminal.service.ts @@ -1,91 +1,114 @@ import { TerminalSetupInfoModel, terminalSetupInfoZodModel } from "../../shared/model/terminal-setup-info.model"; import { DefaultEventsMap, Socket } from "socket.io"; -import setupPodService from "./setup-services/setup-pod.service"; import k3s from "../adapter/kubernetes-api.adapter"; import * as k8s from '@kubernetes/client-node'; import stream from 'stream'; import { StreamUtils } from "@/shared/utils/stream.utils"; import WebSocket from "ws"; +import crypto from 'crypto'; interface TerminalStrean { stdoutStream: stream.PassThrough; stderrStream: stream.PassThrough; stdinStream: stream.PassThrough; - streamInputKey: string; - streamOutputKey: string; - websocket: WebSocket.WebSocket; + terminalSessionKey: string; + websocket?: WebSocket.WebSocket; } export class TerminalService { activeStreams = new Map(); async streamLogs(socket: Socket) { - console.log('Client connected:', socket.id); + console.log('[NEW] Client connected:', socket.id); const streamsOfSocket: TerminalStrean[] = []; socket.on('openTerminal', async (podInfo) => { + console.warn('openTerminal', podInfo); + try { + const terminalInfo = terminalSetupInfoZodModel.parse(podInfo); + if (!terminalInfo.terminalSessionKey) { + console.warn('terminalSessionKey not provided. Setting as undefined.'); + } + console.log(terminalInfo) + const streamInputKey = StreamUtils.getInputStreamName(terminalInfo); + const streamOutputKey = StreamUtils.getOutputStreamName(terminalInfo); - const terminalInfo = terminalSetupInfoZodModel.parse(podInfo); - const streamInputKey = StreamUtils.getInputStreamName(terminalInfo); - const streamOutputKey = StreamUtils.getOutputStreamName(terminalInfo); + /*const podReachable = await setupPodService.waitUntilPodIsRunningFailedOrSucceded(terminalInfo.namespace, terminalInfo.podName); + if (!podReachable) { + socket.emit(streamOutputKey, 'Pod is not reachable.'); + return; + }*/ - const podReachable = await setupPodService.waitUntilPodIsRunningFailedOrSucceded(terminalInfo.namespace, terminalInfo.podName); - if (!podReachable) { - socket.emit(streamOutputKey); - return; + const exec = new k8s.Exec(k3s.getKubeConfig()); + + const stdoutStream = new stream.PassThrough(); + const stderrStream = new stream.PassThrough(); + const stdinStream = new stream.PassThrough(); + console.log('starting exec') + exec.exec( + terminalInfo.namespace, + terminalInfo.podName, + terminalInfo.containerName, + [terminalInfo.terminalType === 'sh' ? '/bin/sh' : '/bin/bash'], + /* process.stdout, + process.stderr, + process.stdin,*/ + stdoutStream, + stderrStream, + stdinStream, + false /* tty */, + (status: k8s.V1Status) => { + console.log('Exited with status:'); + console.log(JSON.stringify(status, null, 2)); + stderrStream!.end(); + stdoutStream!.end(); + stdinStream!.end(); + }, + ); + + stdoutStream.on('data', (chunk) => { + console.log(chunk) + socket.emit(streamOutputKey, chunk.toString()); + }); + stdoutStream.on('error', (error) => { + console.error("Error in terminal stream:", error); + }); + stdoutStream.on('end', () => { + //console.log(`[END] Log stream ended for ${streamKey} by ${streamEndedByClient ? 'client' : 'server'}`); + + }); + + stderrStream.on('data', (chunk) => { + console.log(chunk) + socket.emit(streamOutputKey, chunk.toString()); + }); + socket.on(streamInputKey, (data) => { + console.log('Received data:', data); + stdinStream!.write(data); + }); + + streamsOfSocket.push({ + stdoutStream, + stderrStream, + stdinStream, + terminalSessionKey: terminalInfo.terminalSessionKey ?? '', + //websocket + }); + + console.log(`Client ${socket.id} joined terminal stream for:`); + console.log(`Input: ${streamInputKey}`); + console.log(`Output: ${streamOutputKey}`); + } catch (error) { + console.error('Error while initializing terminal session', podInfo, error); } - - const exec = new k8s.Exec(k3s.getKubeConfig()); - - const stdoutStream = new stream.PassThrough(); - const stderrStream = new stream.PassThrough(); - const stdinStream = new stream.PassThrough(); - - const websocket = await exec.exec( - terminalInfo.namespace, - terminalInfo.podName, - terminalInfo.containerName, - ['/bin/sh'], - process.stdout, - process.stderr, - process.stdin, - /* stdoutStream, - stderrStream, - stdinStream,*/ - true /* tty */, - (status: k8s.V1Status) => { - console.log('Exited with status:'); - console.log(JSON.stringify(status, null, 2)); - stderrStream!.end(); - stdoutStream!.end(); - stdinStream!.end(); - }, - ); - - stdoutStream.on('data', (chunk) => { - console.log(chunk) - socket.emit(streamOutputKey, chunk.toString()); - }); - stderrStream.on('data', (chunk) => { - console.log(chunk) - socket.emit(streamOutputKey, chunk.toString()); - }); - socket.on(streamInputKey, (data) => { - stdinStream!.write(data); - }); - - streamsOfSocket.push({ stdoutStream, stderrStream, stdinStream, streamInputKey, streamOutputKey, websocket }); - - - console.log(`Client ${socket.id} joined terminal stream for ${streamInputKey}`); }); socket.on('closeTerminal', (podInfo) => { + console.warn('closeTerminal', podInfo); const terminalInfo = terminalSetupInfoZodModel.parse(podInfo); - const streamInputKey = StreamUtils.getInputStreamName(terminalInfo); - const streams = streamsOfSocket.find(stream => stream.streamInputKey === streamInputKey); + const streams = streamsOfSocket.find(stream => stream.terminalSessionKey === terminalInfo.terminalSessionKey); if (streams) { this.deleteLogStream(streams); } @@ -101,12 +124,12 @@ export class TerminalService { private deleteLogStream(streams: TerminalStrean) { - /* streams.stderrStream.end(); - streams.stdoutStream.end(); - streams.stdinStream.end(); - streams.websocket.close();*/ + /* streams.stderrStream.end(); + streams.stdoutStream.end(); + streams.stdinStream.end(); + streams.websocket.close();*/ - console.log(`Stopped log stream for ${streams.streamInputKey}.`); + console.log(`Stopped log stream for ${streams.terminalSessionKey}.`); } /* private async createLogStreamForPod(socket: Socket, diff --git a/src/shared/model/terminal-setup-info.model.ts b/src/shared/model/terminal-setup-info.model.ts index a90308f..c7df297 100644 --- a/src/shared/model/terminal-setup-info.model.ts +++ b/src/shared/model/terminal-setup-info.model.ts @@ -4,6 +4,8 @@ export const terminalSetupInfoZodModel = z.object({ namespace: z.string().min(1), podName: z.string().min(1), containerName: z.string().min(1), + terminalType: z.enum(['sh', 'bash']).default('bash').nullish(), + terminalSessionKey: z.string().nullish(), }); export type TerminalSetupInfoModel = z.infer; \ No newline at end of file diff --git a/src/shared/utils/stream.utils.ts b/src/shared/utils/stream.utils.ts index b99bdbc..828c14e 100644 --- a/src/shared/utils/stream.utils.ts +++ b/src/shared/utils/stream.utils.ts @@ -3,10 +3,10 @@ import { TerminalSetupInfoModel } from "../model/terminal-setup-info.model"; export class StreamUtils { static getInputStreamName(terminalInfo: TerminalSetupInfoModel) { - return `${terminalInfo.namespace}_${terminalInfo.podName}_${terminalInfo.containerName}_input`; + return `${terminalInfo.terminalSessionKey}_input`; } static getOutputStreamName(terminalInfo: TerminalSetupInfoModel) { - return `${terminalInfo.namespace}_${terminalInfo.podName}_${terminalInfo.containerName}_output`; + return `${terminalInfo.terminalSessionKey}_output`; } } \ No newline at end of file