feat: connect frontend terminal to terminal service

fix: use ReconnectWebSocket to make the connection more stable

fix: typescript error on importing css in vue
This commit is contained in:
Justin Lin committed 2025-05-05 19:55:35 +08:00
1 parent 9c9456693d
commit c7e6d31b9c
12 files changed
+84 -9

No files matched your search

+3 -2
View File
@@ -22,6 +22,7 @@
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
"marked": "^10.0.0", "marked": "^10.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"reconnecting-websocket": "^4.4.0",
"socket.io-client": "^4.7.1", "socket.io-client": "^4.7.1",
"validator": "^13.11.0", "validator": "^13.11.0",
"vue": "^3.3.4", "vue": "^3.3.4",
@@ -34,12 +35,12 @@
}, },
"devDependencies": { "devDependencies": {
"@types/validator": "^13.11.9", "@types/validator": "^13.11.9",
"@vitejs/plugin-vue": "^5.2.2",
"@vue/compiler-sfc": "^3.3.13", "@vue/compiler-sfc": "^3.3.13",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"@vitejs/plugin-vue": "^5.2.2",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2", "typescript": "~5.7.2",
"vite": "^6.3.1", "vite": "^6.3.1",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
+5 -1
View File
@@ -22,7 +22,11 @@ const tasks = computed(() => {
<div class="h-full w-full max-h-full"> <div class="h-full w-full max-h-full">
<p id="topic" class="text-text font-black" v-if="sessionStore.quest"> <p id="topic" class="text-text font-black" v-if="sessionStore.quest">
{{ sessionStore.quest.title }} {{ sessionStore.quest.title }}
{{ sessionStore.session?.token }} <a
:href="`http://localhost/terminal/${sessionStore.session?.containerName}?token=${sessionStore.session?.token}`"
>
Open Terminal
</a>
</p> </p>
<br /> <br />
<div> <div>
+4
View File
@@ -50,8 +50,12 @@ const useSession = defineStore('session', {
...session, ...session,
graph: new FileGraph(session.graph), graph: new FileGraph(session.graph),
pwd: '/home/commander', pwd: '/home/commander',
containerName: session.containerName || '',
} }
term.reset() term.reset()
term.connect(
`/terminal/${session.containerName}/ws?token=${session.token}`
)
await socket.connect(this.session) await socket.connect(this.session)
term.focus() term.focus()
}, },
+1
View File
@@ -22,4 +22,5 @@ export interface Session {
tasks: ITask[] tasks: ITask[]
responses: IResponse[] responses: IResponse[]
token: string token: string
containerName: string
} }
+4
View File
@@ -0,0 +1,4 @@
declare module '*.css' {
const content: string
export default content
}
+37 -4
View File
@@ -1,10 +1,12 @@
import { Terminal } from 'xterm' import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit' import { FitAddon } from 'xterm-addon-fit'
import ReconnectingWebSocket from 'reconnecting-websocket'
import 'xterm/css/xterm.css' import 'xterm/css/xterm.css'
class SocketTerminal { class SocketTerminal {
private term: Terminal private term: Terminal
private fitAddon: FitAddon private fitAddon: FitAddon
private socket: ReconnectingWebSocket | null = null
constructor() { constructor() {
this.term = new Terminal({ this.term = new Terminal({
@@ -19,6 +21,40 @@ class SocketTerminal {
}) })
this.fitAddon = new FitAddon() this.fitAddon = new FitAddon()
this.term.loadAddon(this.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 { resizeScreen(): void {
@@ -38,16 +74,13 @@ class SocketTerminal {
this.term.write(message) this.term.write(message)
} }
onData(callback: (data: string) => void): void {
this.term.onData(callback)
}
focus(): void { focus(): void {
this.term.focus() this.term.focus()
} }
reset(): void { reset(): void {
this.term.reset() this.term.reset()
this.socket?.close()
} }
} }
+2
View File
@@ -28,6 +28,7 @@ export interface ISession {
user: Types.ObjectId user: Types.ObjectId
quest: string quest: string
containerId: string | null containerId: string | null
containerName: string | null
status: 'active' | 'finished' | 'inactive' status: 'active' | 'finished' | 'inactive'
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
@@ -52,6 +53,7 @@ export const Session = model<ISession>(
required: true, required: true,
}, },
containerId: String, containerId: String,
containerName: String,
status: { status: {
type: String, type: String,
enum: ['active', 'finished', 'inactive'], enum: ['active', 'finished', 'inactive'],
+1
View File
@@ -61,6 +61,7 @@ export async function createNewSession(
user: userId, user: userId,
quest: quest.id, quest: quest.id,
containerId: container.id, containerId: container.id,
containerName,
}) })
console.log('container', container.id) console.log('container', container.id)
+1
View File
@@ -38,6 +38,7 @@ async function sessionDetail(session: ISession) {
responses: gameSession.getResponses(), responses: gameSession.getResponses(),
tasks: gameSession.getTasks(), tasks: gameSession.getTasks(),
graph: gameSession.getGraph(), graph: gameSession.getGraph(),
containerName: session.containerName,
token, token,
} }
} }
+1
View File
@@ -13,6 +13,7 @@ var (
) )
func serveWs(w http.ResponseWriter, r *http.Request) { func serveWs(w http.ResponseWriter, r *http.Request) {
fmt.Println("Serving WS connection")
ws, err := upgrader.Upgrade(w, r, nil) ws, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Println("upgrade:", err) log.Println("upgrade:", err)
+20 -2
View File
@@ -6,8 +6,26 @@ http:
entryPoints: entryPoints:
- web - web
backend: backend-api:
rule: 'PathPrefix(`/api`, `/socket.io`, `/trpc`, `/api-docs`)' 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 service: backend
entryPoints: entryPoints:
- web - web
+5
View File
@@ -8179,6 +8179,11 @@ realize-package-specifier@~3.0.1:
dezalgo "^1.0.1" dezalgo "^1.0.1"
npm-package-arg "^4.1.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: regenerate-unicode-properties@^10.1.0:
version "10.1.1" version "10.1.1"
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz"