Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Add web terminal with reconnecting TTYs#1186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
kylecarbs merged 8 commits intomainfromwebterm
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
PrevPrevious commit
NextNext commit
Active Windows mode on Windows
  • Loading branch information
@kylecarbs
kylecarbs committedApr 29, 2022
commit4ef71062f9f6c8eaa99bb35c023979b46f59210c
2 changes: 2 additions & 0 deletionsagent/agent.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -484,6 +484,8 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, rawID string, conn ne
}
a.reconnectingPTYs.Store(id, rpty)
go func() {
// CommandContext isn't respected for Windows PTYs right now,
// so we need to manually track the lifecycle.
// When the context has been completed either:
// 1. The timeout completed.
// 2. The parent context was canceled.
Expand Down
2 changes: 2 additions & 0 deletionssite/package.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,7 @@
"react-dom": "17.0.2",
"react-router-dom": "6.3.0",
"swr": "1.2.2",
"uuid": "^8.3.2",
"xstate": "4.31.0",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0",
Expand All@@ -65,6 +66,7 @@
"@types/react": "17.0.44",
"@types/react-dom": "17.0.16",
"@types/superagent": "4.1.15",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "5.21.0",
"@typescript-eslint/parser": "5.21.0",
"@xstate/cli": "0.1.7",
Expand Down
1 change: 1 addition & 0 deletionssite/src/api/types.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,6 +87,7 @@ export interface WorkspaceResource {
export interface WorkspaceAgent {
id: string
name: string
operating_system: string
}

export interface APIKeyResponse {
Expand Down
7 changes: 0 additions & 7 deletionssite/src/pages/TerminalPage/TerminalPage.test.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
import { waitFor } from "@testing-library/react"
import crypto from "crypto"
import "jest-canvas-mock"
import WS from "jest-websocket-mock"
import { rest } from "msw"
Expand All@@ -25,12 +24,6 @@ Object.defineProperty(window, "matchMedia", {
})),
})

Object.defineProperty(window, "crypto", {
value: {
randomUUID: () => crypto.randomUUID(),
},
})

Object.defineProperty(window, "TextEncoder", {
value: TextEncoder,
})
Expand Down
66 changes: 33 additions & 33 deletionssite/src/pages/TerminalPage/TerminalPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { makeStyles } from "@material-ui/core/styles"
import { useMachine } from "@xstate/react"
import React from "react"
import { useLocation, useNavigate, useParams } from "react-router-dom"
import { v4 as uuidv4 } from "uuid"
import * as XTerm from "xterm"
import { FitAddon } from "xterm-addon-fit"
import { WebLinksAddon } from "xterm-addon-web-links"
Expand All@@ -16,14 +17,6 @@ export const Language = {
websocketErrorMessagePrefix: "WebSocket failed: ",
}

// TypeScript doesn't have the randomUUID type on Crypto yet. See:
// https://github.com/denoland/deno/issues/12754#issuecomment-970386235
declare global {
interface Crypto {
randomUUID: () => string
}
}

export const TerminalPage: React.FC<{
readonly renderer?: XTerm.RendererType
}> = ({ renderer }) => {
Expand All@@ -39,13 +32,14 @@ export const TerminalPage: React.FC<{
// a round-trip, and must be a UUIDv4.
const [reconnectionToken] = React.useState<string>(() => {
const search = new URLSearchParams(location.search)
let reconnect = search.get("reconnect")
if (reconnect === null) {
reconnect = crypto.randomUUID()
}
return reconnect
return search.get("reconnect") ?? uuidv4()
})
const [terminalState, sendEvent] = useMachine(terminalMachine, {
context: {
reconnection: reconnectionToken,
workspaceName: workspace,
username: username,
},
actions: {
readMessage: (_, event) => {
if (typeof event.data === "string") {
Expand All@@ -59,6 +53,8 @@ export const TerminalPage: React.FC<{
},
})
const isConnected = terminalState.matches("connected")
const { organizationsError, workspaceError, workspaceAgentError, workspaceAgent, websocketError } =
terminalState.context

// Create the terminal!
React.useEffect(() => {
Expand DownExpand Up@@ -125,13 +121,7 @@ export const TerminalPage: React.FC<{
replace: true,
},
)
sendEvent({
type: "CONNECT",
reconnection: reconnectionToken,
workspaceName: workspace,
username: username,
})
}, [location.search, navigate, workspace, username, sendEvent, reconnectionToken])
}, [location.search, navigate, reconnectionToken])

// Apply terminal options based on connection state.
React.useEffect(() => {
Expand All@@ -150,17 +140,17 @@ export const TerminalPage: React.FC<{
terminal.options = {
disableStdin: true,
}
if (terminalState.context.organizationsError instanceof Error) {
terminal.writeln(Language.organizationsErrorMessagePrefix +terminalState.context.organizationsError.message)
if (organizationsError instanceof Error) {
terminal.writeln(Language.organizationsErrorMessagePrefix + organizationsError.message)
}
if (terminalState.context.workspaceError instanceof Error) {
terminal.writeln(Language.workspaceErrorMessagePrefix +terminalState.context.workspaceError.message)
if (workspaceError instanceof Error) {
terminal.writeln(Language.workspaceErrorMessagePrefix + workspaceError.message)
}
if (terminalState.context.workspaceAgentError instanceof Error) {
terminal.writeln(Language.workspaceAgentErrorMessagePrefix +terminalState.context.workspaceAgentError.message)
if (workspaceAgentError instanceof Error) {
terminal.writeln(Language.workspaceAgentErrorMessagePrefix + workspaceAgentError.message)
}
if (terminalState.context.websocketError instanceof Error) {
terminal.writeln(Language.websocketErrorMessagePrefix +terminalState.context.websocketError.message)
if (websocketError instanceof Error) {
terminal.writeln(Language.websocketErrorMessagePrefix + websocketError.message)
}
return
}
Expand All@@ -174,6 +164,7 @@ export const TerminalPage: React.FC<{
terminal.focus()
terminal.options = {
disableStdin: false,
windowsMode: workspaceAgent?.operating_system === "windows",
}

// Update the terminal size post-fit.
Expand All@@ -185,10 +176,11 @@ export const TerminalPage: React.FC<{
},
})
}, [
terminalState.context.workspaceError,
terminalState.context.organizationsError,
terminalState.context.workspaceAgentError,
terminalState.context.websocketError,
workspaceError,
organizationsError,
workspaceAgentError,
websocketError,
workspaceAgent,
terminal,
fitAddon,
isConnected,
Expand All@@ -199,7 +191,9 @@ export const TerminalPage: React.FC<{
<>
{/* This overlay makes it more obvious that the terminal is disconnected. */}
{/* It's nice for situations where Coder restarts, and they are temporarily disconnected. */}
<div className={`${styles.overlay} ${isConnected ? "connected" : ""}`} />
<div className={`${styles.overlay} ${isConnected ? "connected" : ""}`}>
<span>Disconnected</span>
</div>
<div className={styles.terminal} ref={xtermRef} data-testid="terminal" />
</>
)
Expand All@@ -214,6 +208,12 @@ const useStyles = makeStyles(() => ({
bottom: 0,
right: 0,
zIndex: 1,
alignItems: "center",
justifyContent: "center",
display: "flex",
color: "white",
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 18,
backgroundColor: "rgba(0, 0, 0, 0.5)",
"&.connected": {
opacity: 0,
Expand Down
1 change: 1 addition & 0 deletionssite/src/testHelpers/entities.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,6 +102,7 @@ export const MockWorkspace: Workspace = {
export const MockWorkspaceAgent: WorkspaceAgent = {
id: "test-workspace-agent",
name: "a-workspace-agent",
operating_system: "linux",
}

export const MockWorkspaceResource: WorkspaceResource = {
Expand Down
50 changes: 19 additions & 31 deletionssite/src/xServices/terminal/terminalXService.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,7 +48,7 @@ export const terminalMachine =
},
},
id: "terminalState",
initial: "disconnected",
initial: "gettingOrganizations",
states: {
gettingOrganizations: {
invoke: {
Expand DownExpand Up@@ -150,13 +150,13 @@ export const terminalMachine =
{
services: {
getOrganizations: API.getOrganizations,
getWorkspace: async (context: TerminalContext) => {
getWorkspace: async (context) => {
if (!context.organizations || !context.workspaceName) {
throw new Error("organizations or workspace not set")
}
return API.getWorkspace(context.organizations[0].id, context.username, context.workspaceName)
},
getWorkspaceAgent: async (context: TerminalContext) => {
getWorkspaceAgent: async (context) => {
if (!context.workspace || !context.workspaceName) {
throw new Error("workspace or workspace name is not set")
}
Expand All@@ -167,38 +167,29 @@ export const terminalMachine =
const agentName = workspaceNameParts[1]

const resources = await API.getWorkspaceResources(context.workspace.latest_build.id)
for (let i = 0; i < resources.length; i++) {
const resource = resources[i]
if (!resource.agents) {
continue
}
if (resource.agents.length <= 0) {
continue
}
if (!agentName) {
return resource.agents[0]
}
for (let a = 0; a < resource.agents.length; a++) {
const agent = resource.agents[a]
if (agent.name !== agentName) {
continue

const agent = resources
.map((resource) => {
if (!resource.agents || resource.agents.length < 1) {
return
}
return agent
}
if (!agentName) {
return resource.agents[0]
}
return resource.agents.find((agent) => agent.name === agentName)
})
.filter((a) => a)[0]
if (!agent) {
throw new Error("no agent found with id")
}
throw new Error("noagent found with id")
returnagent
},
connect: (context: TerminalContext) => (send) => {
connect: (context) => (send) => {
return new Promise<WebSocket>((resolve, reject) => {
if (!context.workspaceAgent) {
return reject("workspace agent is not set")
}
let proto = location.protocol
if (proto === "https:") {
proto = "wss:"
} else {
proto = "ws:"
}
const proto = location.protocol === "https:" ? "wss:" : "ws:"
const socket = new WebSocket(
`${proto}//${location.host}/api/v2/workspaceagents/${context.workspaceAgent.id}/pty?reconnect=${context.reconnection}`,
)
Expand DownExpand Up@@ -275,9 +266,6 @@ export const terminalMachine =
}
context.websocket.send(new TextEncoder().encode(JSON.stringify(event.request)))
},
readMessage: () => {
// Override this with the terminal writer!
},
disconnect: (context: TerminalContext) => {
// Code 1000 is a successful exit!
context.websocket?.close(1000)
Expand Down
5 changes: 5 additions & 0 deletionssite/yarn.lock
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3194,6 +3194,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==

"@types/uuid@^8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==

"@types/webpack-env@^1.16.0":
version "1.16.3"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp