Skip to content

Commit

Permalink
chore: remove pty ws endpoint (#6045)
Browse files Browse the repository at this point in the history
* chore: remove pty ws endpoint

Maintaining PTY is a pain, especially with node upgrades. See #6044 as
an example.
For Desktop we can implement terminal support in Tauri instead of
Garden.

* chore: remove unneeded hack from rollup related to pty
  • Loading branch information
stefreak committed May 15, 2024
1 parent 31713f2 commit ef1b91e
Show file tree
Hide file tree
Showing 8 changed files with 3 additions and 623 deletions.
3 changes: 1 addition & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@garden-io/garden-terraform": "*",
"@scg82/exit-hook": "^3.4.1",
"chalk": "^5.3.0",
"node-abi": "^3.54.0",
"tar": "^6.2.1",
"undici": "^6.11.1",
"unzipper": "^0.10.14"
Expand All @@ -59,4 +58,4 @@
"lint": "eslint --ext .ts src/",
"test": "mocha"
}
}
}
57 changes: 0 additions & 57 deletions cli/src/build-pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import chalk from "chalk"
import { getAbi } from "node-abi"
import { resolve, relative, join } from "path"
import { STATIC_DIR, GARDEN_CLI_ROOT, GARDEN_CORE_ROOT } from "@garden-io/core/build/src/constants.js"
import { readFile, writeFile } from "fs/promises"
Expand All @@ -17,7 +16,6 @@ import { pick } from "lodash-es"
import minimist from "minimist"
import { createHash } from "node:crypto"
import { createReadStream, createWriteStream } from "fs"
import { makeTempDir } from "@garden-io/core/build/test/helpers.js"
import * as url from "node:url"
import { Readable } from "node:stream"
import { pipeline } from "node:stream/promises"
Expand Down Expand Up @@ -483,61 +481,6 @@ async function pkgCommon({ targetName, spec }: { targetName: string; spec: Targe
await remove(targetPath)
await mkdirp(targetPath)

console.log(` - ${targetName} -> node-pty`)
const abi = getAbi(spec.node, "node")

if (spec.nodeBinaryPlatform === "linux") {
const filename = spec.os === "alpine" ? `node.abi${abi}.musl.node` : `node.abi${abi}.node`
const abiPath = resolve(
GARDEN_CORE_ROOT,
"node_modules",
"@homebridge",
"node-pty-prebuilt-multiarch",
"prebuilds",
`${spec.nodeBinaryPlatform}-${spec.arch}`,
filename
)
await copy(abiPath, resolve(targetPath, "pty.node"))
} else {
const tmpDir = await makeTempDir()
const ptyArchiveFilename = resolve(tmpDir.path, "pty.tar.gz")

// See also https://github.com/homebridge/node-pty-prebuilt-multiarch/releases
const checksums = {
"120-win32-x64": "344921e4036277b1edcbc01d2c7b4a22a3e0a85c911bdf9255fe1168e8e439b6",
"120-darwin-x64": "c406d1ba59ffa750c8a456ae22a75a221eaee2579f3b6f54296e72a5d79c6853",
"120-darwin-arm64": "2818fd6a31dd5889fa9612ceb7ae5ebe5c2422964a4a908d1f05aec120ebccaf",
}

const key = `${abi}-${spec.nodeBinaryPlatform}-${spec.arch}`
const checksum = checksums[key]

if (!checksum) {
throw new Error(
`Missing checksum for ${key}. Needs to be updated when changing the NodeJS version or pty version.`
)
}

await downloadFromWeb({
url: `https://github.com/homebridge/node-pty-prebuilt-multiarch/releases/download/v0.11.8/node-pty-prebuilt-multiarch-v0.11.8-node-v${abi}-${spec.nodeBinaryPlatform}-${spec.arch}.tar.gz`,
checksum,
targetPath: ptyArchiveFilename,
})

// extract
const extractStream = tar.x({
C: targetPath,
// The stripping removes the outer directories, so we end up with the files directly in the target directory.
// Filtering happens first, so it works fine.
strip: 2,
filter: (path) => {
return path.startsWith(`build/Release/`)
},
})
await pipeline(createReadStream(ptyArchiveFilename), extractStream)
await tmpDir.cleanup()
}

if (spec.os === "macos") {
await copy(resolve(GARDEN_CORE_ROOT, "lib", "fsevents", "fsevents.node"), resolve(targetPath, "fsevents.node"))
}
Expand Down
1 change: 0 additions & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"dependencies": {
"@codenamize/codenamize": "^1.1.1",
"@hapi/joi": "git+https://github.com/garden-io/joi.git#master",
"@homebridge/node-pty-prebuilt-multiarch": "0.11.8",
"@jsdevtools/readdir-enhanced": "^6.0.4",
"@kubernetes/client-node": "^1.0.0-rc4",
"@opentelemetry/api": "^1.6.0",
Expand Down
52 changes: 0 additions & 52 deletions core/src/server/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ import {
getRunStatusPayloads,
getTestStatusPayloads,
} from "../actions/helpers.js"
import { z } from "zod"
import { exec } from "../util/util.js"
import split2 from "split2"
import pProps from "p-props"

const autocompleteArguments = {
Expand Down Expand Up @@ -286,55 +283,6 @@ export class _GetActionStatusesCommand extends ConsoleCommand {
}
}

export const shellCommandParamsSchema = z.object({
command: z.string().describe("The executable path to run."),
args: z.array(z.string()).default([]).describe("arguments to pass to the command."),
cwd: z.string().describe("The working directory to run the command in."),
})

const shellCommandArgs = {
request: new StringParameter({
help: "JSON-encoded request object.",
required: true,
}),
}

type ShellCommandArgs = typeof shellCommandArgs

export class _ShellCommand extends ConsoleCommand<ShellCommandArgs> {
name = "_shell"
help = "[Internal] Runs a shell command (used by Desktop client)."
override hidden = true

override enableAnalytics = false
override streamEvents = false
override noProject = true

override arguments = shellCommandArgs

override outputsSchema = () => joi.object()

async action({ log, args: _args }: CommandParams<ShellCommandArgs>): Promise<CommandResult<{}>> {
const { command, args, cwd } = shellCommandParamsSchema.parse(JSON.parse(_args.request))

const outputStream = split2()
outputStream.on("error", () => {})
outputStream.on("data", (line: Buffer) => {
log.info(line.toString())
})

await exec(command, args, {
cwd,
stdout: outputStream,
stderr: outputStream,
env: { ...process.env },
shell: true,
})

return { result: {} }
}
}

export interface BaseServerRequest {
id?: string
command?: string
Expand Down
2 changes: 0 additions & 2 deletions core/src/server/instance-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
HideCommand,
_GetDeployStatusCommand,
_GetActionStatusesCommand,
_ShellCommand,
} from "./commands.js"
import type { GardenInstanceKeyParams } from "./helpers.js"
import { getGardenInstanceKey } from "./helpers.js"
Expand Down Expand Up @@ -124,7 +123,6 @@ export class GardenInstanceManager {
new HideCommand(),
new _GetDeployStatusCommand(),
new _GetActionStatusesCommand(),
new _ShellCommand(),
]),
...(extraCommands || []),
]
Expand Down
106 changes: 2 additions & 104 deletions core/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import Router from "koa-router"
import websockify from "koa-websocket"
import bodyParser from "koa-bodyparser"
import getPort, { portNumbers } from "get-port"
import { isArray, omit } from "lodash-es"
import { omit } from "lodash-es"

import type { BaseServerRequest } from "./commands.js"
import { resolveRequest, serverRequestSchema, shellCommandParamsSchema } from "./commands.js"
import { resolveRequest, serverRequestSchema } from "./commands.js"
import { DEFAULT_GARDEN_DIR_NAME, gardenEnv } from "../constants.js"
import type { Log } from "../logger/log-entry.js"
import type { Command, CommandResult, PrepareParams } from "../commands/base.js"
Expand All @@ -43,15 +43,10 @@ import type { ConfigGraph } from "../graph/config-graph.js"
import { getGardenCloudDomain } from "../cloud/api.js"
import type { ServeCommand } from "../commands/serve.js"
import type { AutocompleteSuggestion } from "../cli/autocomplete.js"
import { z } from "zod"
import { omitUndefined } from "../util/objects.js"
import { createServer } from "http"
import { defaultServerPort } from "../commands/serve.js"

import type PTY from "@homebridge/node-pty-prebuilt-multiarch"
import pty from "@homebridge/node-pty-prebuilt-multiarch"
import { styles } from "../logger/styles.js"
import { commandListToShellScript } from "../util/escape.js"

const skipLogsForCommands = ["autocomplete"]
const serverLogName = "garden-server"
Expand Down Expand Up @@ -547,97 +542,6 @@ export class GardenServer extends EventEmitter {
})
})

wsRouter.get("/pty", async (ctx) => {
const websocket: Koa.Context["websocket"] = ctx["websocket"]

const connectionId = uuidv4()

// args may not just be a single value
if (ctx.query.args && !isArray(ctx.query.args)) {
ctx.query.args = [ctx.query.args]
}

const validation = shellCommandBodySchema.safeParse(ctx.query)

if (!validation.success) {
const event = websocketCloseEvents.badRequest
const msg = `${event.message}: ${validation.error.message}`
// We need to send line returns as \r\n, otherwise the terminal will not work correctly
websocket.send(msg.replace(/\r?\n/g, "\r\n") + "\r\n")
websocket.close(event.code, msg)
return
}

const { command, args, cwd, key, columns, rows } = validation.data

// It's crucial to authenticate here because running shell commands locally is sensitive
if (key !== this.authKey) {
const event = websocketCloseEvents.unauthorized
websocket.close(event.code, event.message)
return
}

let proc: PTY.IPty
let cleanedUp = false

const cleanup = () => {
if (cleanedUp) {
return
}
cleanedUp = true
this.log.info(`Connection ${connectionId} terminated, cleaning up.`)
proc?.kill()
}

try {
// FIXME: Why do we need to sleep for 1 second?
proc = pty.spawn("sh", ["-c", `sleep 1; ${commandListToShellScript([command, ...args])}`], {
name: "xterm-256color",
cols: columns,
rows,
cwd,
env: { ...omitUndefined(process.env) }, // TODO: support this
})

proc.onData((data) => {
websocket.OPEN && websocket.send(data)
})

proc.onExit(({ exitCode, signal }) => {
const msg = `Command '${command}' exited with code ${exitCode}, signal ${signal}`
this.log.info(msg)
if (websocket.OPEN) {
if (exitCode !== 0) {
websocket.send(msg + "\r\n")
} else {
websocket.send(styles.success("\r\n\r\nDone!\r\n"))
}
// We use 4700 + exitCode because the websocket close code must be a number between 4000 and 4999
websocket.close(4700 + exitCode, msg)
}
})

this.setupWsHeartbeat(connectionId, websocket, cleanup)

// Make sure we clean up listeners when connections end.
websocket.on("close", cleanup)

// Stream stdin
websocket.on("message", (stdin: string | Buffer) => {
proc.write(stdin.toString())
})
} catch (err) {
const msg = `Could not run command '${command}': ${err}`
this.log.error(msg)
const event = websocketCloseEvents.ok
if (websocket.OPEN) {
websocket.send(msg + "\r\n")
websocket.close(event.code, msg)
}
cleanup()
}
})

app.ws.use(<Koa.Middleware<any>>wsRouter.routes())
app.ws.use(<Koa.Middleware<any>>wsRouter.allowedMethods())
}
Expand Down Expand Up @@ -991,9 +895,3 @@ type SendWrapper<T extends ServerWebsocketMessageType = ServerWebsocketMessageTy
type: T,
payload: ServerWebsocketMessages[T]
) => void

const shellCommandBodySchema = shellCommandParamsSchema.extend({
key: z.string().describe("The server auth key."),
columns: z.coerce.number().default(80).describe("Number of columns in the virtual terminal."),
rows: z.coerce.number().default(30).describe("Number of rows in the virtual terminal."),
})

0 comments on commit ef1b91e

Please sign in to comment.