Compare commits

...

6 Commits

Author SHA1 Message Date
93c7bd5f53 change terminal 2025-08-11 11:29:50 +08:00
6f4a0ae8c1 change terminal 2025-07-16 22:11:32 +08:00
3dedd24e49 change terminal 2025-07-16 21:35:39 +08:00
ef6a6b3b02 Merge main 2025-07-16 20:03:42 +08:00
b61f7048df fix api bug 2025-05-29 11:13:22 +08:00
4f195b16e0 feature-permission 2025-05-27 23:58:00 +08:00
17 changed files with 557 additions and 382 deletions

View File

@@ -0,0 +1 @@
ls

View File

@@ -2,7 +2,6 @@ package docker
import ( import (
"archive/tar" "archive/tar"
"bytes"
"context" "context"
"fmt" "fmt"
"net" "net"
@@ -223,7 +222,7 @@ func PullImageSync(cli *client.Client, dockerHost string, image string) error {
return nil return nil
} }
func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions) (string, error) { func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions) error {
ctx := context.Background() ctx := context.Background()
// 创建容器配置 // 创建容器配置
config := &container.Config{ config := &container.Config{
@@ -255,41 +254,16 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions
resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.Name) resp, err := cli.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.Name)
if err != nil { if err != nil {
log.Info("fail to create container %v", err) log.Info("fail to create container %v", err)
return "", err return err
} }
// 启动容器 // 启动容器
err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil { if err != nil {
log.Info("fail to start container %v", err) log.Info("fail to start container %v", err)
return "", err return err
} }
// 创建 tar 归档文件 return nil
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加文件到 tar 归档
addFileToTar(tw, "devcontainer_init.sh", opts.InitializeCommand, 0777)
addFileToTar(tw, "devcontainer_restart.sh", opts.RestartCommand, 0777)
//ExecCommandInContainer(&ctx, cli, resp.ID, "touch /home/devcontainer_init.sh && chomd +x /home/devcontainer_init.sh")
err = cli.CopyToContainer(ctx, resp.ID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
if err != nil {
log.Info("%v", err)
return "", err
}
// 获取日志流
out, _ := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: false,
})
defer out.Close()
// 5. 解析日志
var stdoutBuf, stderrBuf bytes.Buffer
_, _ = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out)
return stdoutBuf.String() + "\n" + stderrBuf.String(), nil
} }
// addFileToTar 将文件添加到 tar 归档 // addFileToTar 将文件添加到 tar 归档
@@ -307,3 +281,53 @@ func addFileToTar(tw *tar.Writer, filename string, content string, mode int64) e
} }
return nil return nil
} }
// ImageExists 检查指定镜像是否存在
// 返回值:
// - bool: 镜像是否存在true=存在false=不存在)
// - error: 非空表示检查过程中发生错误
func ImageExists(imageName string) (bool, error) {
// 上下文
ctx := context.Background()
// 创建 Docker 客户端
cli, err := CreateDockerClient(&ctx)
if err != nil {
return false, err // 其他错误
}
// 获取镜像信息
_, _, err = cli.ImageInspectWithRaw(ctx, imageName)
if err != nil {
return false, err // 其他错误
}
return true, nil // 镜像存在
}
func CheckDirExists(cli *client.Client, containerID, dirPath string) (bool, error) {
// 创建 exec 配置
execConfig := types.ExecConfig{
Cmd: []string{"test", "-d", dirPath}, // 检查目录是否存在
AttachStdout: true,
AttachStderr: true,
}
// 创建 exec 实例
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
if err != nil {
return false, fmt.Errorf("创建 exec 实例失败: %v", err)
}
// 执行命令
var exitCode int
err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
if err != nil {
return false, fmt.Errorf("启动 exec 命令失败: %v", err)
}
// 获取命令执行结果
resp, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
if err != nil {
return false, fmt.Errorf("获取 exec 结果失败: %v", err)
}
exitCode = resp.ExitCode
return exitCode == 0, nil // 退出码为 0 表示目录存在
}

View File

@@ -22,6 +22,4 @@ type CreateDevcontainerOptions struct {
PostCreateCommand []string PostCreateCommand []string
Binds []string Binds []string
PortBindings nat.PortMap PortBindings nat.PortMap
InitializeCommand string
RestartCommand string
} }

View File

@@ -0,0 +1,2 @@
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{border:0;height:0;left:-9999em;margin:0;opacity:0;overflow:hidden;padding:0;position:absolute;resize:none;top:0;white-space:nowrap;width:0;z-index:-5}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;bottom:0;cursor:default;left:0;overflow-y:scroll;position:absolute;right:0;top:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{left:0;position:absolute;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;left:-9999em;line-height:normal;position:absolute;top:0;visibility:hidden}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{bottom:0;color:transparent;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:10}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{height:1px;left:-9999px;overflow:hidden;position:absolute;width:1px}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{position:absolute;z-index:6}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{pointer-events:none;position:absolute;right:0;top:0;z-index:8}.xterm-decoration-top{position:relative;z-index:2}.modal{align-items:center;display:flex;overflow:hidden;position:fixed;z-index:40}.modal,.modal-background{bottom:0;left:0;right:0;top:0}.modal-background{background-color:rgba(74,74,74,.8);position:absolute}.modal-content{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}.modal-content .box{background-color:#fff;color:#4a4a4a;display:block;padding:1.25rem}.modal-content header{border-bottom:1px solid #ddd;font-weight:700;margin-bottom:10px;padding-bottom:10px;text-align:center}.modal-content .file-input{height:.01em;left:0;outline:none;position:absolute;top:0;width:.01em}.modal-content .file-cta{align-items:center;background-color:#f5f5f5;border-color:#dbdbdb;border-radius:3px;box-shadow:none;color:#6200ee;cursor:pointer;display:inline-flex;font-size:1em;font-weight:500;height:2.25em;justify-content:flex-start;line-height:1.5;outline:none;padding:calc(.375em - 1px) 1em;position:relative;vertical-align:top;white-space:nowrap}@media print,screen and (min-width:769px){.modal-content{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}body,html{height:100%;margin:0;min-height:100%;overflow:hidden}#terminal-container{height:100%;margin:0 auto;padding:0;width:auto}#terminal-container .terminal{height:calc(100% - 10px);padding:5px}
/*# sourceMappingURL=app.180babeee880c26cc052.css.map*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
/*! crc32.js (C) 2014-present SheetJS -- http://sheetjs.com */

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="mobile-web-app-capable" content="yes"><title>ttyd - Terminal</title><link inline rel="icon" type="image/png" href="favicon.png"><link inline rel="stylesheet" type="text/css" href="app.180babeee880c26cc052.css"></head><body><script inline type="text/javascript" src="app.82d3ff4ffb6a27df9ae0.js"></script></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,25 @@
package devcontainer package devcontainer
import ( import (
clictx "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"io/ioutil"
"net"
"net/http" "net/http"
"net/url"
"path" "path"
"strconv"
"strings" "strings"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer" devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer" devcontainer_service "code.gitea.io/gitea/services/devcontainer"
) )
@@ -28,26 +37,7 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
Actor: ctx.Doer, Actor: ctx.Doer,
Repository: ctx.Repo.Repository, Repository: ctx.Repo.Repository,
} }
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(*ctx)
err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID).
Find(&devContainerOutput)
ctx.Data["isCreatingDevcontainer"] = false
ctx.Data["InitializedContainer"] = true
if err == nil && len(devContainerOutput) > 0 {
ctx.Data["isCreatingDevcontainer"] = true
}
var created bool = false
for _, item := range devContainerOutput {
if item.ListId > 0 && item.Status == "success" {
created = true
}
if item.Status != "success" {
ctx.Data["InitializedContainer"] = false
}
}
ctx.Data["isAdmin"] = false ctx.Data["isAdmin"] = false
if ctx.Doer.IsAdmin { if ctx.Doer.IsAdmin {
ctx.Data["isAdmin"] = true ctx.Data["isAdmin"] = true
@@ -61,37 +51,13 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link() //ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
devContainerMetadata, err := devcontainer_service.GetRepoDevcontainerDetails(ctx, opts) devContainerMetadata, err := devcontainer_service.GetRepoDevcontainerDetails(ctx, opts)
hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0 && created hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0
ctx.Data["HasDevContainer"] = hasDevContainer ctx.Data["HasDevContainer"] = hasDevContainer
ctx.Data["HasDevContainerSetting"] = false ctx.Data["HasDevContainerSetting"] = false
if hasDevContainer { if hasDevContainer {
ctx.Data["DevContainer"] = devContainerMetadata ctx.Data["DevContainer"] = devContainerMetadata
} }
ctx.Data["PullImage"] = false
ctx.Data["CreateContainer"] = false
ctx.Data["Init"] = false
ctx.Data["Running"] = false
ctx.Data["RestartOrStop"] = false
ctx.Data["Save"] = false
switch devContainerMetadata.DevContainerStatus {
case 1:
ctx.Data["PullImage"] = true
case 2:
ctx.Data["CreateContainer"] = true
case 3:
ctx.Data["Init"] = true
case 4:
ctx.Data["Running"] = true
case 5:
ctx.Data["Restart"] = true
case 6:
ctx.Data["Stop"] = true
case 7:
ctx.Data["Save"] = true
default:
log.Info("unknown status")
}
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json // 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx) isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
@@ -114,11 +80,18 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest" ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
} }
// 获取WebSSH服务端口 ctx.Data["TerminalParams"] = "user=" + ctx.Doer.Name + "&repo=" + ctx.Repo.Repository.Name + "&repoid=" + strconv.FormatInt(ctx.Repo.Repository.ID, 10) + "&userid=" + strconv.FormatInt(ctx.Doer.ID, 10)
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, devContainerMetadata.DevContainerName) if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
if err == nil { // 获取WebSSH服务端口
ctx.Data["WebSSHUrl"] = webTerminalURL webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, devContainerMetadata.DevContainerName)
if err == nil {
ctx.Data["WebSSHUrl"] = webTerminalURL
}
} else {
ctx.Data["WebSSHUrl"] = "http://localhost:3000/assets/terminal/index.html?type=docker&" + fmt.Sprintf("%v", ctx.Data["TerminalParams"])
} }
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, &devContainerMetadata) terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, &devContainerMetadata)
if err == nil { if err == nil {
ctx.Data["VSCodeUrl"] = "vscode" + terminalURL ctx.Data["VSCodeUrl"] = "vscode" + terminalURL
@@ -310,6 +283,8 @@ func DeleteRepoDevContainerForCurrentActor(ctx *context.Context) {
type OutputResponse struct { type OutputResponse struct {
CurrentJob struct { CurrentJob struct {
IP string `json:"ip"`
Port string `json:"port"`
Title string `json:"title"` Title string `json:"title"`
Detail string `json:"detail"` Detail string `json:"detail"`
Steps []*ViewJobStep `json:"steps"` Steps []*ViewJobStep `json:"steps"`
@@ -329,28 +304,74 @@ type ViewStepLogLine struct {
} }
func GetContainerOutput(ctx *context.Context) { func GetContainerOutput(ctx *context.Context) {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
query := ctx.Req.URL.Query()
var paramInfo = ParamInfo{
RepoID: query.Get("repo"),
UserID: query.Get("user"),
}
var devContainerOutput []devcontainer_models.DevcontainerOutput var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(*ctx) dbEngine := db.GetEngine(*ctx)
err := dbEngine.Table("devcontainer_output"). var status string
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID). _, err := db.GetEngine(ctx).
Table("devcontainer").
Select("devcontainer_status").
Where("user_id = ? AND repo_id = ?", paramInfo.UserID, paramInfo.RepoID).
Get(&status)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
var containerName string
_, err = db.GetEngine(ctx).
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", paramInfo.UserID, paramInfo.RepoID).
Get(&containerName)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", paramInfo.UserID, paramInfo.RepoID).
Find(&devContainerOutput) Find(&devContainerOutput)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
return return
} }
if len(devContainerOutput) > 0 { if len(devContainerOutput) > 0 {
resp := &OutputResponse{} resp := &OutputResponse{}
resp.CurrentJob.Title = ctx.Repo.Repository.Name + " Devcontainer Info" resp.CurrentJob.Title = ctx.Repo.Repository.Name + " Devcontainer Info"
resp.CurrentJob.Detail = "success" resp.CurrentJob.Detail = status
for _, item := range devContainerOutput { if status == "4" {
if item.Status != "success" { // 获取WebSSH服务端口
resp.CurrentJob.Detail = "running" webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, containerName)
if devContainerOutput[0].Status == "success" && devContainerOutput[1].Status == "success" { if err == nil {
resp.CurrentJob.Detail = "created" log.Info("URL解析失败: %v", err)
}
} }
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
log.Info("URL解析失败: %v", err)
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
resp.CurrentJob.IP = terminalHost
resp.CurrentJob.Port = terminalPort
if err != nil {
log.Info("URL解析失败: %v", err)
}
}
for _, item := range devContainerOutput {
logLines := []ViewStepLogLine{} logLines := []ViewStepLogLine{}
logLines = append(logLines, ViewStepLogLine{ logLines = append(logLines, ViewStepLogLine{
Index: 1, Index: 1,
@@ -399,3 +420,221 @@ func StopContainer(ctx *context.Context) {
ctx.JSON(http.StatusOK, map[string]string{}) ctx.JSON(http.StatusOK, map[string]string{})
} }
type ParamInfo struct {
RepoID string
UserID string
}
func GetCommand(ctx *context.Context) {
query := ctx.Req.URL.Query()
var commandInfo = ParamInfo{
RepoID: query.Get("repo"),
UserID: query.Get("user"),
}
dbEngine := db.GetEngine(*ctx)
var status uint16
var containerName string
var containerID uint16
var work_dir string
var cmd string
var flag uint16
//当前状态
_, err := dbEngine.
Table("devcontainer").
Select("devcontainer_status, id, name, devcontainer_work_dir").
Where("user_id = ? AND repo_id = ?", commandInfo.UserID, commandInfo.RepoID).
Get(&status, &containerID, &containerName, &work_dir)
if err != nil {
log.Info("Error: %v\n", err)
}
flag = status
switch status {
case 0:
if containerID > 0 {
flag = 1
}
case 1:
devContainerJson, err := devcontainer_service.GetDevcontainerJsonModel(ctx, ctx.Repo.Repository)
if err != nil {
log.Info("Error : %v\n", err)
break
}
buffer, err := docker_module.ImageExists(devContainerJson.Image)
if err != nil {
log.Info("Error : %v\n", err)
break
}
if buffer {
flag = 2
}
case 2:
// 创建docker client
cliCtx := clictx.Background()
cli, err := docker_module.CreateDockerClient(&cliCtx)
if err != nil {
break
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
containerStatus, err := docker_module.GetContainerStatus(cli, containerID)
if containerStatus == "running" {
flag = 3
}
case 3:
// 创建docker client
cliCtx := clictx.Background()
cli, err := docker_module.CreateDockerClient(&cliCtx)
if err != nil {
break
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
dirStatus, err := docker_module.CheckDirExists(cli, containerID, work_dir)
if dirStatus {
flag = 4
}
case 4:
cmd = "docker exec -it --workdir " + work_dir + " " + containerName + " /bin/bash -c \"echo 'Successfully connected to the container';bash\"\n"
default:
log.Info("other status")
}
if flag != status {
//下一条指令
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", commandInfo.UserID, commandInfo.RepoID, flag).
Get(&cmd)
if err != nil {
log.Info("Error : %v\n", err)
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", commandInfo.UserID, commandInfo.RepoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: flag})
if err != nil {
log.Info("err %v", err)
}
}
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": fmt.Sprint(flag)})
}
func GetTerminalToken(ctx *context.Context) {
query := ctx.Req.URL.Query()
var containerName string
_, err := db.GetEngine(ctx).
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", query.Get("user"), query.Get("repo")).
Get(&containerName)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
if containerName == "" {
ctx.Error(404, "Service not found")
}
// 获取WebSSH服务端口
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, containerName)
if err == nil {
log.Info("URL解析失败: %v", err)
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
log.Info("URL解析失败: %v", err)
return
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
if err != nil {
log.Info("URL解析失败: %v", err)
return
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("http://" + terminalHost + ":" + terminalPort + "/token")
if err != nil {
log.Error("Failed to connect terminal: %v", err)
ctx.Error(http.StatusInternalServerError, "Failed to connect terminal")
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("Failed to read response body: %v", err)
ctx.Error(http.StatusInternalServerError, "Failed to read response body")
return
}
ctx.Resp.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
ctx.Status(resp.StatusCode)
_, _ = ctx.Write(body)
}
func GetContainerStatus(ctx *context.Context) {
var status string
var id int
var containerName string
var flag uint16
log.Info("GetContainerStatus %v 666 %v", ctx.Doer.ID, ctx.Repo.Repository.ID)
_, err := db.GetEngine(ctx).
Table("devcontainer").
Select("devcontainer_status, id, name").
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID).
Get(&status, &id, &containerName)
if err != nil {
log.Info("Error: %v\n", err)
}
if id == 0 {
status = "-"
}
switch status {
case "5":
// 创建docker client
cliCtx := clictx.Background()
cli, err := docker_module.CreateDockerClient(&cliCtx)
if err != nil {
break
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
containerStatus, err := docker_module.GetContainerStatus(cli, containerID)
if containerStatus == "running" {
flag = 4
}
case "6":
// 创建docker client
cliCtx := clictx.Background()
cli, err := docker_module.CreateDockerClient(&cliCtx)
if err != nil {
break
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
containerStatus, err := docker_module.GetContainerStatus(cli, containerID)
if containerStatus == "exited" {
flag = 8
}
default:
log.Info("other status")
}
if fmt.Sprintf("%d", flag) != status {
_, err = db.GetEngine(ctx).Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", ctx.Doer.ID, ctx.Repo.Repository.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: flag})
if err != nil {
log.Info("err %v", err)
}
}
ctx.JSON(http.StatusOK, map[string]string{"status": status})
}

View File

@@ -474,6 +474,13 @@ func registerRoutes(m *web.Router) {
// Especially some AJAX requests, we can reduce middleware number to improve performance. // Especially some AJAX requests, we can reduce middleware number to improve performance.
m.Get("/", Home) m.Get("/", Home)
m.Methods("OPTIONS", "/command", func(ctx *context.Context) {
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "GET, POST")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
})
m.Get("/devstar-home", DevstarHome) m.Get("/devstar-home", DevstarHome)
m.Get("/sitemap.xml", sitemapEnabled, ignExploreSignIn, HomeSitemap) m.Get("/sitemap.xml", sitemapEnabled, ignExploreSignIn, HomeSitemap)
m.Group("/.well-known", func() { m.Group("/.well-known", func() {
@@ -733,7 +740,6 @@ func registerRoutes(m *web.Router) {
m.Get("", admin.Dashboard) m.Get("", admin.Dashboard)
m.Get("/system_status", admin.SystemStatus) m.Get("/system_status", admin.SystemStatus)
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost) m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
m.Post("/logo", web.Bind(forms.AvatarForm{}), admin.LogoPost)
m.Get("/self_check", admin.SelfCheck) m.Get("/self_check", admin.SelfCheck)
m.Post("/self_check", admin.SelfCheckPost) m.Post("/self_check", admin.SelfCheckPost)
@@ -1355,14 +1361,17 @@ func registerRoutes(m *web.Router) {
m.Group("", func() { m.Group("", func() {
m.Combo("").Get(devcontainer_web.GetRepoDevContainerDetails) m.Combo("").Get(devcontainer_web.GetRepoDevContainerDetails)
}, context.RepoRef()) }, context.RepoRef())
m.Get("/command", devcontainer_web.GetCommand)
m.Get("/status", devcontainer_web.GetContainerStatus)
m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration) m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration)
m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer
m.Get("/output", devcontainer_web.GetContainerOutput) m.Get("/output", devcontainer_web.GetContainerOutput)
m.Get("/restart", devcontainer_web.RestartContainer) m.Get("/restart", devcontainer_web.RestartContainer)
m.Get("/stop", devcontainer_web.StopContainer) m.Get("/stop", devcontainer_web.StopContainer)
m.Get("/terminalToken", devcontainer_web.GetTerminalToken)
m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor) m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor)
m.Post("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor) m.Post("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor)
}, },
// 进入 Dev Container 编辑页面需要符合条件: // 进入 Dev Container 编辑页面需要符合条件:
// 1. 已登录 // 1. 已登录

View File

@@ -454,25 +454,7 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
devcontainerApp.Status.ExtraPortsAssigned) devcontainerApp.Status.ExtraPortsAssigned)
return "", fmt.Errorf("ttyd port (7681) not found for container: %s", devcontainerName) return "", fmt.Errorf("ttyd port (7681) not found for container: %s", devcontainerName)
case setting.DOCKER: case setting.DOCKER:
cli, err := docker.CreateDockerClient(&ctx) return "http://localhost:7681/?", nil
if err != nil {
return "", err
}
defer cli.Close()
containerID, err := docker.GetContainerID(cli, devcontainerName)
if err != nil {
return "", err
}
port, err := docker.GetMappedPort(cli, containerID, "7681")
if err != nil {
return "", err
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
return "", err
}
return "http://" + cfg.Section("server").Key("DOMAIN").Value() + ":" + port + "/", nil
default: default:
return "", fmt.Errorf("unknown agent") return "", fmt.Errorf("unknown agent")
} }
@@ -943,7 +925,7 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc
parsedURL.Host = newHost parsedURL.Host = newHost
// 生成git仓库的 URL // 生成git仓库的 URL
newURL := parsedURL.String() newURL := parsedURL.String()
newDevContainer.GitRepositoryURL = newURL
// Read the init script from file // Read the init script from file
var initializeScriptContent, restartScriptContent []byte var initializeScriptContent, restartScriptContent []byte
_, err = os.Stat("devcontainer_init.sh") _, err = os.Stat("devcontainer_init.sh")

View File

@@ -2,11 +2,9 @@ package devcontainer
import ( import (
"archive/tar" "archive/tar"
"bufio"
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"os/exec"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -16,6 +14,7 @@ import (
devcontainer_models "code.gitea.io/gitea/models/devcontainer" devcontainer_models "code.gitea.io/gitea/models/devcontainer"
docker_module "code.gitea.io/gitea/modules/docker" docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_web_context "code.gitea.io/gitea/services/context" gitea_web_context "code.gitea.io/gitea/services/context"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -33,12 +32,9 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine
return err return err
} }
defer cli.Close() defer cli.Close()
// 加入22 集合
// 加入22 7681集合
natPort22 := nat.Port("22/tcp") natPort22 := nat.Port("22/tcp")
natPort7681 := nat.Port("7681/tcp")
devContainerJSON.ForwardPorts[natPort22] = struct{}{} devContainerJSON.ForwardPorts[natPort22] = struct{}{}
devContainerJSON.ForwardPorts[natPort7681] = struct{}{}
// 2. 创建容器 // 2. 创建容器
opts := &docker_module.CreateDevcontainerOptions{ opts := &docker_module.CreateDevcontainerOptions{
DockerfileContent: newDevContainer.DockerfileContent, DockerfileContent: newDevContainer.DockerfileContent,
@@ -56,8 +52,6 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine
ContainerEnv: devContainerJSON.ContainerEnv, ContainerEnv: devContainerJSON.ContainerEnv,
PostCreateCommand: append([]string{"/home/devcontainer_init.sh"}, devContainerJSON.PostCreateCommand...), PostCreateCommand: append([]string{"/home/devcontainer_init.sh"}, devContainerJSON.PostCreateCommand...),
ForwardPorts: devContainerJSON.ForwardPorts, ForwardPorts: devContainerJSON.ForwardPorts,
InitializeCommand: initializeScript,
RestartCommand: restartScript,
} }
var flag string var flag string
for _, content := range devContainerJSON.RunArgs { for _, content := range devContainerJSON.RunArgs {
@@ -250,261 +244,46 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp
} }
func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error { func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error {
var stdoutScanner, stderrScanner *bufio.Scanner
// 创建扫描器来读取输出
dbEngine := db.GetEngine(*ctx) dbEngine := db.GetEngine(*ctx)
_, err := dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 1})
if err != nil {
log.Info("err %v", err)
}
if opts.DockerfileContent != "" {
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
content := []byte(opts.DockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
panic(err)
}
if _, err := tw.Write(content); err != nil {
panic(err)
}
// 执行镜像构建
opts.Image = fmt.Sprintf("%d", opts.UserId) + "-" + fmt.Sprintf("%d", opts.RepoId) + "-dockerfileimage"
buildOptions := types.ImageBuildOptions{
Tags: []string{opts.Image}, // 镜像标签
}
buildResponse, err := cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
log.Info(err.Error())
return err
}
stdoutScanner = bufio.NewScanner(buildResponse.Body)
} else {
script := "docker " + "-H " + dockerHost + " pull " + opts.Image
cmd := exec.Command("sh", "-c", script)
// 获取标准输出和标准错误输出的管道
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
stdoutScanner = bufio.NewScanner(stdout)
stderrScanner = bufio.NewScanner(stderr)
}
var pullImageOutput = devcontainer_models.DevcontainerOutput{
Output: "",
ListId: 0,
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Pull Image",
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&pullImageOutput); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Get(&pullImageOutput)
if err != nil {
log.Info("err %v", err)
return err
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "", Output: "",
Status: "running", ListId: 1,
Status: "waitting",
UserId: opts.UserId, UserId: opts.UserId,
RepoId: opts.RepoId, RepoId: opts.RepoId,
Command: "Initialize Workspace", Command: "docker " + "-H " + dockerHost + " pull " + opts.Image + "\n",
ListId: 1,
}); err != nil { }); err != nil {
log.Info("Failed to insert record: %v", err) log.Info("Failed to insert record: %v", err)
return err return err
} }
_, err = dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "", Output: "",
Status: "running", Status: "waitting",
UserId: opts.UserId, UserId: opts.UserId,
RepoId: opts.RepoId, RepoId: opts.RepoId,
Command: "Initialize DevStar", Command: `docker run -d --name ` + opts.Name + ` -p 22 ` + opts.Image + ` /bin/bash -c "tail -f /dev/null"` + "\n",
ListId: 2, ListId: 2,
}) }); err != nil {
if err != nil {
log.Info("Failed to insert record: %v", err) log.Info("Failed to insert record: %v", err)
return err return err
} }
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if len(opts.PostCreateCommand) > 1 { if err != nil {
_, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
Output: "", return err
Status: "running", }
UserId: opts.UserId, if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
RepoId: opts.RepoId, Output: "",
Command: "Run postCreateCommand", Status: "waitting",
ListId: 3, UserId: opts.UserId,
}) RepoId: opts.RepoId,
if err != nil { Command: `docker exec ` + opts.Name + ` sh -c "echo \"` + cfg.Section("server").Key("DOMAIN").Value() + ` host.docker.internal\" | tee -a /etc/hosts;apt update;apt install -y git ; git clone ` + opts.GitRepositoryURL + " " + "/data/workspace" + ` "` + "\n",
log.Info("Failed to insert record: %v", err) ListId: 3,
return err }); err != nil {
} log.Info("Failed to insert record: %v", err)
return err
} }
// 使用 goroutine 来读取标准输出
go func() {
var output string
var cur int = 0
for stdoutScanner.Scan() {
output += "\n" + stdoutScanner.Text()
cur++
if cur%10 == 0 {
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
}
}
if stderrScanner != nil {
for stderrScanner.Scan() {
output += "\n" + stderrScanner.Text()
cur++
if cur%10 == 0 {
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
}
}
}
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{Status: "success"})
// 创建并启动容器
_, err := dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 2})
if err != nil {
log.Info("err %v", err)
}
output, err = docker_module.CreateAndStartContainer(cli, opts)
if err != nil {
log.Info("创建或启动容器失败: %v", err)
}
containerID, err := docker_module.GetContainerID(cli, opts.Name)
if err != nil {
log.Info("获取容器ID:%v", err)
}
portInfo, err := docker_module.GetAllMappedPort(cli, containerID)
if err != nil {
log.Info("创建或启动容器失败:%v", err)
}
// 存储到数据库
if _, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 1).
Update(&devcontainer_models.DevcontainerOutput{
Output: output + portInfo,
Status: "success",
}); err != nil {
log.Info("Error storing output for command %v: %v\n", opts.CommandList[2], err)
}
// 创建 exec 实例
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 3})
if err != nil {
log.Info("err %v", err)
}
var buffer string = ""
var state int = 2
for index, cmd := range opts.PostCreateCommand {
if index == 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
buffer = ""
state = 3
continue
}
output, err = docker_module.ExecCommandInContainer(ctx, cli, containerID, cmd)
buffer += output
if err != nil {
log.Info("执行命令失败:%v", err)
}
_, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Output: buffer,
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command pull image: %v\n", err)
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
if err != nil {
log.Info("err %v", err)
}
}()
return nil return nil
} }
func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevContainer) error { func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevContainer) error {

View File

@@ -74,7 +74,7 @@ func RegistRunner(ctx context.Context, token string) error {
Binds: binds, Binds: binds,
ContainerEnv: env, ContainerEnv: env,
} }
_, err = docker_module.CreateAndStartContainer(cli, opts) err = docker_module.CreateAndStartContainer(cli, opts)
if err != nil { if err != nil {
return fmt.Errorf("创建并注册Runner失败:%v", err) return fmt.Errorf("创建并注册Runner失败:%v", err)
} }

View File

@@ -37,10 +37,10 @@
</div> </div>
</form> </form>
<iframe src="{{.WebSSHUrl}}" width="100%" style="height: 100vh;" frameborder="0">您的浏览器不支持iframe</iframe>
</div> </div>
<div id="repo-devcontainer-view" data-outputLink="{{.Repository.Link}}/dev-container/output"></div> <!--<div id="repo-devcontainer-view" data-outputLink="{{.Repository.Link}}/dev-container/output"></div>-->
{{end}} {{end}}
</div> </div>
@@ -52,37 +52,27 @@
<div class="ui relaxed list"> <div class="ui relaxed list">
{{if .HasDevContainer}} {{if .HasDevContainer}}
<div class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/dev-container/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div> <div style=" display: none;" id="deleteContainer" class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/dev-container/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
{{if .isAdmin}} {{if .isAdmin}}
<div style=" display: none;" id="updateContainer" class="item"><a class="delete-button flex-text-inline" style="color:black; " data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
{{end}}
{{if .Running}} <div style=" display: none;" id="restartContainer" class="item"><button id="restartButton" class="flex-text-inline" style="color:black; " onclick="handleClick(event, '{{.Repository.Link}}/dev-container/restart', document.getElementById('stopButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Restart Dev Container</button></div>
<div class="item"><a class="delete-button flex-text-inline" style="color:black;" data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div> <div style=" display: none;" id="stopContainer" class="item"><button id="stopButton" class="flex-text-inline" style="color:black; " onclick="handleClick(event, '{{.Repository.Link}}/dev-container/stop', document.getElementById('restartButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Stop Dev Container</button></div>
{{end}}
{{end}}
{{if .InitializedContainer}}
{{if not .Restart}}
{{if not .Stop}}
<div class="item"><button id="restartButton" class="flex-text-inline" style="color:black;" onclick="handleClick(event, '{{.Repository.Link}}/dev-container/restart', document.getElementById('stopButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Restart Dev Container</button></div>
<div class="item"><button id="stopButton" class="flex-text-inline" style="color:black;" onclick="handleClick(event, '{{.Repository.Link}}/dev-container/stop', document.getElementById('restartButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Stop Dev Container</button></div>
<div class="item"><a class="flex-text-inline" style="color:black;" href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div> <div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div> <div style=" display: none;" id="vsTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div> <div style=" display: none;" id="cursorTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div>
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div> <div style=" display: none;" id="windsurfTerminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div>
{{end}}
{{end}}
{{end}}
{{else if .HasValidDevContainerJSON}} {{else if .HasValidDevContainerJSON}}
<div class="item"> <div class="item">
{{if not .isCreatingDevcontainer}} <div>
{{if .canRead}} {{if .canRead}}
<a class="button flex-text-inline" href="{{.Repository.Link}}/dev-container/create">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</a> <a class="button flex-text-inline" href="{{.Repository.Link}}/dev-container/create">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</a>
{{else}} {{else}}
<div class="button flex-text-inline" disabled>Permission Denied</div> <div class="button flex-text-inline" disabled>Permission Denied</div>
{{end}} {{end}}
{{else}} </div>
<div class="button flex-text-inline" disabled>{{svg "octicon-terminal" 14 "tw-mr-2"}} Running</div>
{{end}}
</div> </div>
{{end}} {{end}}
{{if not .HasValidDevContainerJSON}} {{if not .HasValidDevContainerJSON}}
@@ -229,5 +219,145 @@ function handleClick(event, targetLink, other) {
location.reload(); location.reload();
}); });
} }
</script>
<script>
var status = '-'
var intervalID
const deleteContainer = document.getElementById('deleteContainer');
const updateContainer = document.getElementById('updateContainer');
const restartContainer = document.getElementById('restartContainer');
const stopContainer = document.getElementById('stopContainer');
const webTerminal = document.getElementById('webTerminal');
const vsTerminal = document.getElementById('vsTerminal');
const cursorTerminal = document.getElementById('cursorTerminal');
const windsurfTerminal = document.getElementById('windsurfTerminal');
function concealElement(){
if (deleteContainer){
deleteContainer.style.display = 'none';
}
if (updateContainer) {
updateContainer.style.display = 'none';
}
if (restartContainer) {
restartContainer.style.display = 'none';
}
if (stopContainer) {
stopContainer.style.display = 'none';
}
if (webTerminal) {
webTerminal.style.display = 'none';
}
if (vsTerminal) {
vsTerminal.style.display = 'none';
}
if (cursorTerminal) {
cursorTerminal.style.display = 'none';
}
if (windsurfTerminal) {
windsurfTerminal.style.display = 'none';
}
}
function displayElement(){
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
if (restartContainer) {
restartContainer.style.display = 'block';
}
if (stopContainer) {
stopContainer.style.display = 'block';
}
if (webTerminal) {
webTerminal.style.display = 'block';
}
if (vsTerminal) {
vsTerminal.style.display = 'block';
}
if (cursorTerminal) {
cursorTerminal.style.display = 'block';
}
if (windsurfTerminal) {
windsurfTerminal.style.display = 'block';
}
}
function getStatus() {
fetch(
'{{.Repository.Link}}'+'/dev-container/status'
)
.then(response => response.json())
.then(data => {
if (data.status == '-') {
clearInterval(intervalID);
} else if (data.status == '0' || data.status == '1' || data.status == '2') {
concealElement();
}else if (data.status == '3') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
}else if (data.status == '4') {
displayElement();
clearInterval(intervalID);
}else if (data.status == '5') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
}else if (data.status == '6') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
}else if (data.status == '7') {
concealElement();
}else if (data.status == '8') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
if (restartContainer) {
restartContainer.style.display = 'block';
}
clearInterval(intervalID);
}
})
.catch(error => {
console.error('Error:', error);
});
}
intervalID = setInterval(getStatus, 3000);
if (restartContainer) {
restartContainer.addEventListener('click', function(event) {
// 处理点击逻辑
intervalID = setInterval(getStatus, 3000);
});
}
if (stopContainer) {
stopContainer.addEventListener('click', function(event) {
// 处理点击逻辑
intervalID = setInterval(getStatus, 3000);
});
}
</script> </script>
{{template "base/footer" .}} {{template "base/footer" .}}