diff --git a/app/package.json b/app/package.json index 7fdc36f..c44e96f 100644 --- a/app/package.json +++ b/app/package.json @@ -22,6 +22,7 @@ "dompurify": "^3.2.4", "marked": "^10.0.0", "pinia": "^2.1.7", + "reconnecting-websocket": "^4.4.0", "socket.io-client": "^4.7.1", "validator": "^13.11.0", "vue": "^3.3.4", @@ -34,12 +35,12 @@ }, "devDependencies": { "@types/validator": "^13.11.9", + "@vitejs/plugin-vue": "^5.2.2", "@vue/compiler-sfc": "^3.3.13", + "@vue/tsconfig": "^0.7.0", "autoprefixer": "^10.4.14", "postcss": "^8.4.31", "tailwindcss": "^3.3.3", - "@vitejs/plugin-vue": "^5.2.2", - "@vue/tsconfig": "^0.7.0", "typescript": "~5.7.2", "vite": "^6.3.1", "vue-tsc": "^2.2.8" diff --git a/app/src/components/game/QuestPart.vue b/app/src/components/game/QuestPart.vue index abffd60..7ef9630 100644 --- a/app/src/components/game/QuestPart.vue +++ b/app/src/components/game/QuestPart.vue @@ -22,7 +22,11 @@ const tasks = computed(() => {

{{ sessionStore.quest.title }} - {{ sessionStore.session?.token }} + + Open Terminal +


diff --git a/app/src/store/session.ts b/app/src/store/session.ts index c701142..d19469b 100644 --- a/app/src/store/session.ts +++ b/app/src/store/session.ts @@ -50,8 +50,12 @@ const useSession = defineStore('session', { ...session, graph: new FileGraph(session.graph), pwd: '/home/commander', + containerName: session.containerName || '', } term.reset() + term.connect( + `/terminal/${session.containerName}/ws?token=${session.token}` + ) await socket.connect(this.session) term.focus() }, diff --git a/app/src/types.ts b/app/src/types.ts index ec4a6ea..a057d56 100644 --- a/app/src/types.ts +++ b/app/src/types.ts @@ -22,4 +22,5 @@ export interface Session { tasks: ITask[] responses: IResponse[] token: string + containerName: string } diff --git a/app/src/types/css.d.ts b/app/src/types/css.d.ts new file mode 100644 index 0000000..8be70df --- /dev/null +++ b/app/src/types/css.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: string + export default content +} diff --git a/app/src/utils/terminal.ts b/app/src/utils/terminal.ts index b384844..a585900 100644 --- a/app/src/utils/terminal.ts +++ b/app/src/utils/terminal.ts @@ -1,10 +1,12 @@ import { Terminal } from 'xterm' import { FitAddon } from 'xterm-addon-fit' +import ReconnectingWebSocket from 'reconnecting-websocket' import 'xterm/css/xterm.css' class SocketTerminal { private term: Terminal private fitAddon: FitAddon + private socket: ReconnectingWebSocket | null = null constructor() { this.term = new Terminal({ @@ -19,6 +21,40 @@ class SocketTerminal { }) this.fitAddon = new FitAddon() this.term.loadAddon(this.fitAddon) + this.term.onData((data) => { + if (this.socket) { + this.socket.send(data) + } + }) + } + + connect(url: string): void { + console.log('Connecting to terminal', url) + this.socket = new ReconnectingWebSocket(url) + this.socket.onopen = () => { + console.log('Connected to terminal') + this.term.write('\r\n\x1B[1;32mConnected to terminal\x1B[0m\r\n') + } + + this.socket.onclose = () => { + console.log('Disconnected from terminal') + this.term.write('\r\n\x1B[1;31mDisconnected from terminal\x1B[0m\r\n') + } + + this.socket.onerror = (error) => { + console.error('WebSocket error:', error) + this.term.write( + `\r\n\x1B[1;31mWebSocket error: ${'message' in error ? error.message : ''}\x1B[0m\r\n` + ) + } + this.socket.onmessage = (event) => { + console.log('Received message from terminal') + const data = + typeof event.data === 'string' + ? event.data + : new TextDecoder().decode(event.data) + this.term.write(data) + } } resizeScreen(): void { @@ -38,16 +74,13 @@ class SocketTerminal { this.term.write(message) } - onData(callback: (data: string) => void): void { - this.term.onData(callback) - } - focus(): void { this.term.focus() } reset(): void { this.term.reset() + this.socket?.close() } } diff --git a/packages/models/session.ts b/packages/models/session.ts index cae81e8..30f7d4e 100644 --- a/packages/models/session.ts +++ b/packages/models/session.ts @@ -28,6 +28,7 @@ export interface ISession { user: Types.ObjectId quest: string containerId: string | null + containerName: string | null status: 'active' | 'finished' | 'inactive' createdAt: Date updatedAt: Date @@ -52,6 +53,7 @@ export const Session = model( required: true, }, containerId: String, + containerName: String, status: { type: String, enum: ['active', 'finished', 'inactive'], diff --git a/server/src/models/sessionManager.ts b/server/src/models/sessionManager.ts index c6bb814..046235d 100644 --- a/server/src/models/sessionManager.ts +++ b/server/src/models/sessionManager.ts @@ -61,6 +61,7 @@ export async function createNewSession( user: userId, quest: quest.id, containerId: container.id, + containerName, }) console.log('container', container.id) diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts index c5dad08..3c5ecbf 100644 --- a/server/src/routers/sessionRouter.ts +++ b/server/src/routers/sessionRouter.ts @@ -38,6 +38,7 @@ async function sessionDetail(session: ISession) { responses: gameSession.getResponses(), tasks: gameSession.getTasks(), graph: gameSession.getGraph(), + containerName: session.containerName, token, } } diff --git a/services/terminal-service/main.go b/services/terminal-service/main.go index da3b6b7..9d1ee88 100644 --- a/services/terminal-service/main.go +++ b/services/terminal-service/main.go @@ -13,6 +13,7 @@ var ( ) func serveWs(w http.ResponseWriter, r *http.Request) { + fmt.Println("Serving WS connection") ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("upgrade:", err) diff --git a/traefik/conf/dev.yml b/traefik/conf/dev.yml index dd0d99f..a73b692 100644 --- a/traefik/conf/dev.yml +++ b/traefik/conf/dev.yml @@ -6,8 +6,26 @@ http: entryPoints: - web - backend: - rule: 'PathPrefix(`/api`, `/socket.io`, `/trpc`, `/api-docs`)' + backend-api: + rule: 'PathPrefix(`/api`)' + service: backend + entryPoints: + - web + + backend-socket: + rule: 'PathPrefix(`/socket.io`)' + service: backend + entryPoints: + - web + + backend-trpc: + rule: 'PathPrefix(`/trpc`)' + service: backend + entryPoints: + - web + + backend-api-docs: + rule: 'PathPrefix(`/api-docs`)' service: backend entryPoints: - web diff --git a/yarn.lock b/yarn.lock index 3c13f5b..ee06019 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8179,6 +8179,11 @@ realize-package-specifier@~3.0.1: dezalgo "^1.0.1" npm-package-arg "^4.1.1" +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== + regenerate-unicode-properties@^10.1.0: version "10.1.1" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz"