mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
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:
1 parent
9c9456693d
commit
c7e6d31b9c
12 files changed
+84
-9
No files matched your search
+3
-2
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ export interface Session {
|
|||||||
tasks: ITask[]
|
tasks: ITask[]
|
||||||
responses: IResponse[]
|
responses: IResponse[]
|
||||||
token: string
|
token: string
|
||||||
|
containerName: string
|
||||||
}
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.css' {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user