Repository: 1Panel-dev/1Panel
Pull request #8301: fix: Modify the container terminal connection mode
Author: login: ssongliu
Labels: name: do-not-merge/release-note-label-needed
name: dev
name: approved
Created: 2025-04-02T09:46:58Z | Merged: 2025-04-02T10:19:54Z



Diff:
diff --git a/backend/app/api/v1/terminal.go b/backend/app/api/v1/terminal.go
index 5d32248b9bb0..cdb73a1ca582 100644
--- a/backend/app/api/v1/terminal.go
+++ b/backend/app/api/v1/terminal.go
@@ -96,7 +96,7 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
 	}
 	source := c.Query("source")
 	var containerID string
-	var initCmd string
+	var initCmd []string
 	switch source {
 	case "redis":
 		containerID, initCmd, err = loadRedisInitCmd(c)
@@ -113,11 +113,11 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
 		return
 	}
 	pidMap := loadMapFromDockerTop(containerID)
-	slave, err := terminal.NewCommand("clear && " + initCmd)
+	slave, err := terminal.NewCommand(initCmd)
 	if wshandleError(wsConn, err) {
 		return
 	}
-	defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap)
+	defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
 	defer slave.Close()
 
 	tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
@@ -137,62 +137,63 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
 
 }
 
-func loadRedisInitCmd(c *gin.Context) (string, string, error) {
+func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
 	name := c.Query("name")
 	from := c.Query("from")
-	commands := "redis-cli"
+	commands := []string{"exec", "-it"}
 	database, err := databaseService.Get(name)
 	if err != nil {
-		return "", "", fmt.Errorf("no such database in db, err: %v", err)
+		return "", nil, fmt.Errorf("no such database in db, err: %v", err)
 	}
 	if from == "local" {
 		redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
 		if err != nil {
-			return "", "", fmt.Errorf("no such app in db, err: %v", err)
+			return "", nil, fmt.Errorf("no such app in db, err: %v", err)
 		}
 		name = redisInfo.ContainerName
+		commands = append(commands, []string{name, "redis-cli"}...)
 		if len(database.Password) != 0 {
-			commands = "redis-cli -a " + database.Password + " --no-auth-warning"
+			commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
 		}
 	} else {
-		commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
+		name = "1Panel-redis-cli-tools"
+		commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
 		if len(database.Password) != 0 {
-			commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
+			commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
 		}
-		name = "1Panel-redis-cli-tools"
 	}
-	return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
+	return name, commands, nil
 }
 
-func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
+func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
 	name := c.Query("name")
 	if cmd.CheckIllegal(name) {
-		return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
+		return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
 	}
 	ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
 	if err != nil {
-		return "", "", fmt.Errorf("no such app in db, err: %v", err)
+		return "", nil, fmt.Errorf("no such app in db, err: %v", err)
 	}
 	containerName := ollamaInfo.ContainerName
-	return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
+	return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
 }
 
-func loadContainerInitCmd(c *gin.Context) (string, string, error) {
+func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
 	containerID := c.Query("containerid")
 	command := c.Query("command")
 	user := c.Query("user")
 	if cmd.CheckIllegal(user, containerID, command) {
-		return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
+		return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
 	}
 	if len(command) == 0 || len(containerID) == 0 {
-		return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
+		return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
 	}
-	command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
+	commands := []string{"exec", "-it", containerID, command}
 	if len(user) != 0 {
-		command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
+		commands = []string{"exec", "-it", "-u", user, containerID, command}
 	}
 
-	return containerID, command, nil
+	return containerID, commands, nil
 }
 
 func wshandleError(ws *websocket.Conn, err error) bool {
diff --git a/backend/utils/terminal/local_cmd.go b/backend/utils/terminal/local_cmd.go
index eaf96c6fcc58..d77bb359652c 100644
--- a/backend/utils/terminal/local_cmd.go
+++ b/backend/utils/terminal/local_cmd.go
@@ -25,8 +25,8 @@ type LocalCommand struct {
 	pty *os.File
 }
 
-func NewCommand(initCmd string) (*LocalCommand, error) {
-	cmd := exec.Command("bash")
+func NewCommand(initCmd []string) (*LocalCommand, error) {
+	cmd := exec.Command("docker", initCmd...)
 	if term := os.Getenv("TERM"); term != "" {
 		cmd.Env = append(os.Environ(), "TERM="+term)
 	} else {
@@ -38,11 +38,6 @@ func NewCommand(initCmd string) (*LocalCommand, error) {
 		return nil, errors.Wrapf(err, "failed to start command")
 	}
 
-	if len(initCmd) != 0 {
-		time.Sleep(100 * time.Millisecond)
-		_, _ = pty.Write([]byte(initCmd + "\n"))
-	}
-
 	lcmd := &LocalCommand{
 		closeSignal:  DefaultCloseSignal,
 		closeTimeout: DefaultCloseTimeout,


Reviews:
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T09:47:30Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T09:47:44Z
user: login: wanghe-fit2cloud
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:19:00Z

Review comments:
diff_hunk: @@ -137,62 +137,63 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
 
 }
 
-func loadRedisInitCmd(c *gin.Context) (string, string, error) {
+func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
 	name := c.Query("name")
 	from := c.Query("from")
-	commands := "redis-cli"
+	commands := []string{"exec", "-it"}
 	database, err := databaseService.Get(name)
 	if err != nil {
-		return "", "", fmt.Errorf("no such database in db, err: %v", err)
+		return "", nil, fmt.Errorf("no such database in db, err: %v", err)
 	}
 	if from == "local" {
 		redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
 		if err != nil {
-			return "", "", fmt.Errorf("no such app in db, err: %v", err)
+			return "", nil, fmt.Errorf("no such app in db, err: %v", err)
 		}
 		name = redisInfo.ContainerName
+		commands = append(commands, []string{name, "redis-cli"}...)
 		if len(database.Password) != 0 {
-			commands = "redis-cli -a " + database.Password + " --no-auth-warning"
+			commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
 		}
 	} else {
-		commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
+		name = "1Panel-redis-cli-tools"
+		commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
 		if len(database.Password) != 0 {
-			commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
+			commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
 		}
-		name = "1Panel-redis-cli-tools"
 	}
-	return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
+	return name, commands, nil
 }
 
-func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
+func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
 	name := c.Query("name")
 	if cmd.CheckIllegal(name) {
-		return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
+		return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
 	}
 	ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
 	if err != nil {
-		return "", "", fmt.Errorf("no such app in db, err: %v", err)
+		return "", nil, fmt.Errorf("no such app in db, err: %v", err)
 	}
 	containerName := ollamaInfo.ContainerName
-	return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
+	return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
 }
 
-func loadContainerInitCmd(c *gin.Context) (string, string, error) {
+func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
 	containerID := c.Query("containerid")
 	command := c.Query("command")
 	user := c.Query("user")
 	if cmd.CheckIllegal(user, containerID, command) {
-		return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
+		return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
 	}
 	if len(command) == 0 || len(containerID) == 0 {
-		return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
+		return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
 	}
-	command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
+	commands := []string{"exec", "-it", containerID, command}
 	if len(user) != 0 {
-		command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
+		commands = []string{"exec", "-it", "-u", user, containerID, command}
 	}
 
-	return containerID, command, nil
+	return containerID, commands, nil
 }
 
 func wshandleError(ws *websocket.Conn, err error) bool {
path: backend/app/api/v1/terminal.go
user: login: wanghe-fit2cloud
body: The provided code seems generally clean and well-structured, but there are a few potential improvements:

### Potential Improvements

1. **Code Duplication**:
   - The `loadRedisInitCmd`, `loadOllamaInitCmd`, and `loadContainerInitCmd` functions have similar logic. Consider creating a common function to handle these cases.

```go
func loadAppInitCmd(c *gin.Context, appName string, connectionType string) (string, []string, error) {
    var (
        commands     []string
        name          string
        additionalArgs []string
    )

    // Get database if it exists
    if connectionType == "database" {
        dbName := c.Query("db-name")
        dbInstance, err := databaseService.Get(dbName)
        if err != nil {
            return "", nil, fmt.Errorf("no such database in DB,err: %v", err)
        }
        name = dbInstance.ContainerName + "-" + appName
        additionalArgs = append(additionalArgs, "-a", dbInstance.Password, "--no-auth-warning")
    } else {
        name = appName
        addressQuery := c.Query("address")
        portQuery := c Query("port")

        // Construct the base command based on the type of app
        switch connectionType {
        case "redis":
            addresses := strings.Split(addressQuery, ",")
            ports := strings.Split(portQuery, ",")

            for i := range addresses {
                addrs[i] += fmt.Sprintf(":%v", ports[i])
            }

            if len(ports[0]) > 0 {
                commands = append(commands, "docker exec -it "+name+" redis-cli -h "+fmt.Join(addresses, " "))
            }
        case "ollama":
            name = "1Panel-"+appName+"-tools"
            args := []interface{}{"-a", ""}

            if password, _ := c.Cookie(appName); password != "" {
                args = append(args, "-a", password)
            }

            commands = append(commands, []string{
                "exec",
                "-it",
                name,
                "ollama",
                "run",
            })

            commands = append(commands, append([]string{}, args...)...)
        default:
            return "", nil, fmt.Errorf("unsupported app type: %s", connectionType)
        }
    }

    return name, commands, nil
}
```

2. **Function Naming Consistency**:
   - Some function names, like `wshandleError`, could be more descriptive or consistent with other methods. For example, `handleWebSocketError`.

3. **Security Best Practices**:
   - Ensure that all environment variables and input parameters are sanitized and validated properly to prevent injection attacks.

4. **Logging**:
   - Add logging statements to capture errors and debug information for easier troubleshooting.

5. **Performance Optimization**:
   - If possible, optimize the performance of critical sections, especially around Docker interactions and WebSocket handling.

Overall, while minor changes can enhance readability and maintainability, the core functionality appears robust.
created_at: 2025-04-02T09:47:30Z
author_association: MEMBER
diff_hunk: @@ -38,11 +38,6 @@ func NewCommand(initCmd string) (*LocalCommand, error) {
 		return nil, errors.Wrapf(err, "failed to start command")
 	}
 
-	if len(initCmd) != 0 {
-		time.Sleep(100 * time.Millisecond)
-		_, _ = pty.Write([]byte(initCmd + "\n"))
-	}
-
 	lcmd := &LocalCommand{
 		closeSignal:  DefaultCloseSignal,
 		closeTimeout: DefaultCloseTimeout,
path: backend/utils/terminal/local_cmd.go
user: login: wanghe-fit2cloud
body: There are a couple of improvements and corrections that can be made to this function:

1. The `exec.Command` constructor should now accept the arguments directly without using ellipsis (`...`). Since it's dealing with strings in an array, passing them directly is more appropriate.

2. Replace usage of `DefaultCloseSignal` and `DefaultCloseTimeout`. In newer versions of Go (after September 1st, 2021), these constants have been deprecated and replaced by context cancellation functions like `context.Background()` and `time.NewTimer`.

Here's how you could refactor the relevant part based on these points:

```go
import (
	"context"
	"os"
	"syscall"
	"time"

	execCtx "golang.org/x/sys/exec/exec_ctx"
)

const (
	DefaultCloseContext = context.Background() // Assuming this was a typo as described above
)

func Run(ctx context.Context, dockerCmd ...string) error {
	cmd := execCtx.WithDirectIO(exec.Command("docker", dockerCmd...))
	
	errChan := make(chan error, 1)
	go func(cmd *exec.Cmd) {
		defer close(errChan)
		errChan <- cmd.Run()
	}(cmd)
	
	select {
	case err := <-errChan:
		return err
	case <-ctx.Done():
		stdinPipe, stdinError := cmd.StdinPipe()
		
		if stdinErr == nil && ctx.Err() != context.Canceled {
			_, _ = stdinPipe.Write([]byte(strings.Join(dockerCmd, "\n") + "\n")) // Ensure each arg followed by a newline
			
			readStderr(&stdinPipe, logrus.Errorf)
			os.Exit(127)
		}
		
		return ctx.Err()
	default:
		time.Sleep(time.Second) // Wait for some seconds before proceeding further, optional depending on use-case
		cmd.Process.Signal(syscall.SIGTERM)
		return ctx.Err()
	}
}

// Example usage:
// runContext, cancelFunc := context.WithCancel(context.Background())
// defer cancelFunc()

// if err := Run(runContext, "-itd", image); err != nil {
//    	logrus.Fatalf("Failed running Docker: %s", err)
/// }
```

This refactored version uses structured contexts where appropriate and provides more detailed error handling around user input processing when starting processes inside a container via direct I/O operations. It also includes comments explaining important parts of what the function does, which helps clarify its purpose and functionality.
created_at: 2025-04-02T09:47:43Z
author_association: MEMBER

PR comments:
user: login: f2c-ci-robot[bot]
created_at: 2025-04-02T09:47:02Z
body: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-02T09:48:10Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8301) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8301&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8301&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=1Panel-dev_1Panel&pullRequest=8301&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8301&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8301&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8301)


author_association: NONE
user: login: wanghe-fit2cloud
created_at: 2025-04-02T10:19:11Z
body: /approve
author_association: MEMBER
user: login: f2c-ci-robot[bot]
created_at: 2025-04-02T10:19:20Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/1Panel-dev/1Panel/pull/8301#issuecomment-2772110884" title="Approved">wanghe-fit2cloud</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=1Panel-dev%2F1Panel).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/1Panel-dev/1Panel/blob/dev/OWNERS)~~ [wanghe-fit2cloud]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE