From 0b08a9a5d9fc47ce15a4c95cca204d8314531778 Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Wed, 30 Apr 2025 13:02:14 +0800 Subject: [PATCH] feat: init cmd-hook and move terminal-service to services directory feat: put cmd-hook in container feat: cmd-hook request type, auto restart setup feat: handle ls --- .gitignore | 2 + packages/game/schema/command.ts | 2 +- quests/.includes/zshrc | 2 +- quests/Dockerfile | 27 ++-- server/src/containers/docker.ts | 10 +- services/cmd-hook/.air.toml | 52 +++++++ services/cmd-hook/command.go | 142 ++++++++++++++++++ services/cmd-hook/go.mod | 3 + services/cmd-hook/main.go | 83 ++++++++++ services/cmd-hook/utils.go | 39 +++++ .../terminal-service}/.air.toml | 0 .../terminal-service}/README.md | 0 .../terminal-service}/go.mod | 2 +- .../terminal-service}/go.sum | 0 .../terminal-service}/index.html | 0 .../terminal-service}/main.go | 0 16 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 services/cmd-hook/.air.toml create mode 100644 services/cmd-hook/command.go create mode 100644 services/cmd-hook/go.mod create mode 100644 services/cmd-hook/main.go create mode 100644 services/cmd-hook/utils.go rename {terminal-service => services/terminal-service}/.air.toml (100%) rename {terminal-service => services/terminal-service}/README.md (100%) rename {terminal-service => services/terminal-service}/go.mod (72%) rename {terminal-service => services/terminal-service}/go.sum (100%) rename {terminal-service => services/terminal-service}/index.html (100%) rename {terminal-service => services/terminal-service}/main.go (100%) diff --git a/.gitignore b/.gitignore index 0b6a78d..5bc2bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ config .quest tmp +services/terminal-service/terminal-service +bin # Logs logs diff --git a/packages/game/schema/command.ts b/packages/game/schema/command.ts index b11633b..c2655f3 100644 --- a/packages/game/schema/command.ts +++ b/packages/game/schema/command.ts @@ -21,7 +21,7 @@ export const commandSchema = z.object({ output: z.string().optional(), error: z.string().optional(), pwd: z.string().optional(), - params: commandParamsSchema.optional(), + params: commandParamsSchema.optional().nullable(), }) export type ICommand = z.infer diff --git a/quests/.includes/zshrc b/quests/.includes/zshrc index f2835f7..82cf55c 100644 --- a/quests/.includes/zshrc +++ b/quests/.includes/zshrc @@ -29,7 +29,7 @@ precmd() { export CMD_ERROR_FILE=/tmp/cmd.error # Now run your Node.js script with the result as an argument - node /usr/local/lib/container/cli.js; rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null + /usr/local/bin/cmd-hook; rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null # Reset variables unset CMD_START_TIME diff --git a/quests/Dockerfile b/quests/Dockerfile index 99bfbab..833db7e 100644 --- a/quests/Dockerfile +++ b/quests/Dockerfile @@ -1,4 +1,16 @@ -FROM debian:bookworm-slim +FROM golang:1.24-alpine as builder + +WORKDIR /app + +COPY ./services/cmd-hook/go.mod ./ + +RUN go mod download + +COPY ./services/cmd-hook ./ + +RUN go build -o cmd-hook + +FROM debian:bookworm-slim as main LABEL maintainer="wancat@wancat.cc" @@ -8,18 +20,7 @@ RUN apt-get update && apt-get install -y \ curl \ openssh-server - -RUN curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh -RUN bash nodesource_setup.sh -RUN apt-get install -y nodejs -RUN npm install -g yarn -RUN node -v && yarn -v - -WORKDIR /usr/local/lib/container -COPY ./packages/container/package.json ./yarn.lock ./ -RUN yarn install --modules-folder /usr/src/node_modules -ENV NODE_PATH=/usr/src/node_modules -COPY ./packages/container /usr/local/lib/container +COPY --from=builder /app/cmd-hook /usr/local/bin/cmd-hook COPY ./quests/.includes/zshrc /etc/zsh/zshrc RUN rm -f /etc/motd /etc/issue /etc/issue.net /etc/update-motd.d/* RUN rm -rf /etc/skel && mkdir -p /etc/skel diff --git a/server/src/containers/docker.ts b/server/src/containers/docker.ts index b5c7592..9e4557a 100644 --- a/server/src/containers/docker.ts +++ b/server/src/containers/docker.ts @@ -32,7 +32,7 @@ export async function createContainer( questId: string, imageId: string ): Promise { - let binds: string[] = [] + const binds: string[] = [] if (await questHomeExists(questId)) { logger.info('Mounting quest home', questId) binds.push( @@ -41,14 +41,14 @@ export async function createContainer( } if (config.docker.mountQuest && imageId !== 'base') { logger.info('Mounting quest directory', questId) - binds = [ - `${config.docker.hostProjectRoot}/quests/${questId}/home:/home/commander`, - ] + binds.push( + `${config.docker.hostProjectRoot}/quests/${questId}/home:/home/commander` + ) } if (config.docker.mountCLI) { logger.info('Mounting CLI', questId) binds.push( - `${config.docker.hostProjectRoot}/packages/container:/usr/local/lib/container` + `${config.docker.hostProjectRoot}/services/cmd-hook/bin:/usr/local/bin:ro` ) } const option = newContainerOptions(name, imageId, { binds }) diff --git a/services/cmd-hook/.air.toml b/services/cmd-hook/.air.toml new file mode 100644 index 0000000..d4bd6a8 --- /dev/null +++ b/services/cmd-hook/.air.toml @@ -0,0 +1,52 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./bin/cmd-hook" + cmd = "go build -o ./bin/cmd-hook ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + silent = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/services/cmd-hook/command.go b/services/cmd-hook/command.go new file mode 100644 index 0000000..53bc88a --- /dev/null +++ b/services/cmd-hook/command.go @@ -0,0 +1,142 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type CommandHandler func(cmd string) (CommandParams, error) + +type CommandParams struct { + Discover []FileObject `json:"discover,omitempty"` + Add []FileObject `json:"add,omitempty"` + Remove []FileObject `json:"remove,omitempty"` + Pwd string `json:"pwd,omitempty"` +} + +type FileObject struct { + Path string `json:"path"` + Type FileType `json:"type"` + Discovered bool `json:"discovered"` + Empty bool `json:"empty,omitempty"` +} + +var commandHandlers = map[string]CommandHandler{ + "ls": handleLs, + "cd": handleCd, + "touch": handleTouch, + "mkdir": handleMkdir, + "cp": handleCp, + "rm": handleRm, + "rmdir": handleRmdir, + "mv": handleMv, +} + +func handleCommand(cmd string) (CommandParams, error) { + parts := strings.Fields(cmd) + if len(parts) == 0 { + return CommandParams{}, nil + } + + handler, exists := commandHandlers[parts[0]] + if !exists { + return CommandParams{}, nil + } + + return handler(cmd) +} + +func handleCd(cmd string) (CommandParams, error) { + return CommandParams{ + Pwd: os.Getenv("PWD"), + }, nil +} + +func handleLs(cmd string) (CommandParams, error) { + fmt.Println("handleLs", cmd) + parts := strings.Fields(cmd)[1:] + path := "." + if len(parts) > 1 { + path = parts[1] + } + + files := make([]FileObject, 0) + err := discover(&files, path, 0) + if err != nil { + return CommandParams{}, err + } + + return CommandParams{ + Discover: files, + }, nil +} + +func discover(files *[]FileObject, path string, level int) error { + absPath := resolvePath(path) + fileType, err := fileExists(absPath) + if err != nil { + return fmt.Errorf("ls: cannot access '%s': %v", path, err) + } + + if fileType == FileTypeFile { + *files = append(*files, FileObject{ + Path: absPath, + Type: fileType, + Discovered: level <= 1, + }) + return nil + } + + entries, err := os.ReadDir(absPath) + if err != nil { + return fmt.Errorf("error reading directory: %v", err) + } + + *files = append(*files, FileObject{ + Path: absPath, + Type: fileType, + Discovered: level <= 1, + Empty: len(entries) == 0, + }) + + if level >= 2 { + return nil + } + + for _, entry := range entries { + discover(files, filepath.Join(absPath, entry.Name()), level+1) + } + return nil +} + +func handleTouch(cmd string) (CommandParams, error) { + // TODO: Implement touch command + return CommandParams{}, nil +} + +func handleMkdir(cmd string) (CommandParams, error) { + // TODO: Implement mkdir command + return CommandParams{}, nil +} + +func handleCp(cmd string) (CommandParams, error) { + // TODO: Implement cp command + return CommandParams{}, nil +} + +func handleRm(cmd string) (CommandParams, error) { + // TODO: Implement rm command + return CommandParams{}, nil +} + +func handleRmdir(cmd string) (CommandParams, error) { + // TODO: Implement rmdir command + return CommandParams{}, nil +} + +func handleMv(cmd string) (CommandParams, error) { + // TODO: Implement mv command + return CommandParams{}, nil +} diff --git a/services/cmd-hook/go.mod b/services/cmd-hook/go.mod new file mode 100644 index 0000000..f8d6225 --- /dev/null +++ b/services/cmd-hook/go.mod @@ -0,0 +1,3 @@ +module github.com/linux-odyssey/linux-odyssey/services/cmd-hook + +go 1.24.2 diff --git a/services/cmd-hook/main.go b/services/cmd-hook/main.go new file mode 100644 index 0000000..f298f7f --- /dev/null +++ b/services/cmd-hook/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +type CommandPayload struct { + Command string `json:"command"` + Output string `json:"output"` + Error string `json:"error"` + ExitCode string `json:"exit_code"` + PWD string `json:"pwd"` + Params CommandParams `json:"params"` +} + +func readOrNone(file string) string { + data, err := os.ReadFile(file) + if err != nil { + return "" + } + return string(data) +} + +func main() { + cmdName := os.Getenv("CMD_NAME") + if cmdName == "" { + os.Exit(0) + } + + payload := CommandPayload{ + Command: cmdName, + Output: readOrNone(os.Getenv("CMD_OUTPUT_FILE")), + Error: readOrNone(os.Getenv("CMD_ERROR_FILE")), + ExitCode: os.Getenv("CMD_EXIT_CODE"), + PWD: os.Getenv("PWD"), + } + + params, err := handleCommand(cmdName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error handling command: %v\n", err) + os.Exit(1) + } + + payload.Params = params + + apiEndpoint := os.Getenv("API_ENDPOINT") + token := os.Getenv("TOKEN") + + jsonData, err := json.MarshalIndent(payload, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling payload: %v\n", err) + os.Exit(1) + } + + fmt.Println(string(jsonData)) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/commands", apiEndpoint), bytes.NewBuffer(jsonData)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err) + os.Exit(1) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Fprintf(os.Stderr, "Error sending request: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + fmt.Fprintf(os.Stderr, "Failed to send command: %s\nResponse Body: %s\n", resp.Status, string(body)) + os.Exit(1) + } +} diff --git a/services/cmd-hook/utils.go b/services/cmd-hook/utils.go new file mode 100644 index 0000000..efc2ca5 --- /dev/null +++ b/services/cmd-hook/utils.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "path/filepath" +) + +type FileType string + +const ( + FileTypeFile FileType = "file" + FileTypeDir FileType = "directory" +) + +func resolvePath(path string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(os.Getenv("PWD"), path) +} + +func fileExists(path string) (FileType, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return "", err + } + if fileInfo.IsDir() { + return FileTypeDir, nil + } + return FileTypeFile, nil +} + +func isDir(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return info.IsDir() +} diff --git a/terminal-service/.air.toml b/services/terminal-service/.air.toml similarity index 100% rename from terminal-service/.air.toml rename to services/terminal-service/.air.toml diff --git a/terminal-service/README.md b/services/terminal-service/README.md similarity index 100% rename from terminal-service/README.md rename to services/terminal-service/README.md diff --git a/terminal-service/go.mod b/services/terminal-service/go.mod similarity index 72% rename from terminal-service/go.mod rename to services/terminal-service/go.mod index 8530e2c..ca153ab 100644 --- a/terminal-service/go.mod +++ b/services/terminal-service/go.mod @@ -1,4 +1,4 @@ -module github.com/linux-odyssey/linux-odyssey/terminal-service +module github.com/linux-odyssey/linux-odyssey/services/terminal-service go 1.24.2 diff --git a/terminal-service/go.sum b/services/terminal-service/go.sum similarity index 100% rename from terminal-service/go.sum rename to services/terminal-service/go.sum diff --git a/terminal-service/index.html b/services/terminal-service/index.html similarity index 100% rename from terminal-service/index.html rename to services/terminal-service/index.html diff --git a/terminal-service/main.go b/services/terminal-service/main.go similarity index 100% rename from terminal-service/main.go rename to services/terminal-service/main.go