mirror of
https://github.com/fankes/komari-agent.git
synced 2025-10-18 10:39:24 +08:00
feat: 增强终端实例终止逻辑
This commit is contained in:
@@ -4,6 +4,7 @@ package terminal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,28 +14,37 @@ import (
|
|||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newTerminalImpl 创建一个新的终端实例。
|
||||||
|
// 它会尝试根据用户配置文件查找默认 shell,如果失败则回退到常见 shell。
|
||||||
|
// 优先以交互模式启动 shell,如果不支持则回退到非交互模式。
|
||||||
func newTerminalImpl() (*terminalImpl, error) {
|
func newTerminalImpl() (*terminalImpl, error) {
|
||||||
shell := ""
|
shell := ""
|
||||||
// 从 /etc/passwd 获取
|
// 从 /etc/passwd 获取用户默认 shell
|
||||||
user, err := os.UserHomeDir() // 当前用户
|
userHomeDir, err := os.UserHomeDir() // 获取当前用户的主目录
|
||||||
if err == nil {
|
if err == nil {
|
||||||
passwd, err := os.ReadFile("/etc/passwd")
|
passwdContent, err := os.ReadFile("/etc/passwd")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, line := range strings.Split(string(passwd), "\n") {
|
for _, line := range strings.Split(string(passwdContent), "\n") {
|
||||||
if strings.Contains(line, user) {
|
if strings.Contains(line, userHomeDir) {
|
||||||
parts := strings.Split(line, ":")
|
parts := strings.Split(line, ":")
|
||||||
if len(parts) >= 7 && parts[6] != "" {
|
if len(parts) >= 7 && parts[6] != "" {
|
||||||
shell = parts[6]
|
shell = parts[6]
|
||||||
|
log.Printf("Found shell from /etc/passwd: %s for user home: %s\n", shell, userHomeDir)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Error reading /etc/passwd: %v\n", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Error getting user home directory: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 shell 是否可用
|
// 验证从 /etc/passwd 获取的 shell 是否可用
|
||||||
if shell != "" {
|
if shell != "" {
|
||||||
if _, err := exec.LookPath(shell); err != nil {
|
if _, err := exec.LookPath(shell); err != nil {
|
||||||
|
log.Printf("Shell '%s' from /etc/passwd not found in PATH, falling back.\n", shell)
|
||||||
shell = "" // 默认 shell 不可用,清空以进入回退逻辑
|
shell = "" // 默认 shell 不可用,清空以进入回退逻辑
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,26 +52,31 @@ func newTerminalImpl() (*terminalImpl, error) {
|
|||||||
// 回退到默认 shell 列表
|
// 回退到默认 shell 列表
|
||||||
defaultShells := []string{"zsh", "bash", "sh"}
|
defaultShells := []string{"zsh", "bash", "sh"}
|
||||||
if shell == "" {
|
if shell == "" {
|
||||||
|
log.Println("Shell not found or invalid, trying default shells.")
|
||||||
for _, s := range defaultShells {
|
for _, s := range defaultShells {
|
||||||
if _, err := exec.LookPath(s); err == nil {
|
if _, err := exec.LookPath(s); err == nil {
|
||||||
shell = s
|
shell = s
|
||||||
|
log.Printf("Using default shell: %s\n", shell)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if shell == "" {
|
if shell == "" {
|
||||||
return nil, fmt.Errorf("no supported shell found")
|
return nil, fmt.Errorf("no supported shell found among %v", defaultShells)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建进程: 优先使用交互模式,如不支持则回退
|
// 创建进程: 优先使用交互模式,如不支持则回退
|
||||||
cmd := exec.Command(shell, "-i")
|
cmd := exec.Command(shell, "-i") // 尝试以交互模式启动
|
||||||
cmd.Env = append(os.Environ(), // 继承系统环境变量
|
cmd.Env = append(os.Environ(), // 继承系统环境变量
|
||||||
"TERM=xterm-256color",
|
"TERM=xterm-256color", // 设置终端类型,提高兼容性
|
||||||
"LANG=C.UTF-8",
|
"LANG=C.UTF-8", // 设置语言环境为 UTF-8
|
||||||
"LC_ALL=C.UTF-8",
|
"LC_ALL=C.UTF-8", // 强制所有本地化变量为 UTF-8
|
||||||
)
|
)
|
||||||
|
|
||||||
tty, err := pty.Start(cmd)
|
tty, err := pty.Start(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("Failed to start pty with -i (%s -i): %v. Retrying without -i.\n", shell, err)
|
||||||
// 交互模式不被支持,回退到无 -i 的启动方式
|
// 交互模式不被支持,回退到无 -i 的启动方式
|
||||||
cmd = exec.Command(shell)
|
cmd = exec.Command(shell)
|
||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
@@ -87,50 +102,91 @@ func newTerminalImpl() (*terminalImpl, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unixTerminal 实现了 Unix 系统下的终端接口。
|
||||||
type unixTerminal struct {
|
type unixTerminal struct {
|
||||||
tty *os.File
|
tty *os.File // 伪终端设备文件
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd // 启动的 shell 进程命令
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close 关闭终端,并尝试优雅地终止 shell 进程及其子进程。
|
||||||
func (t *unixTerminal) Close() error {
|
func (t *unixTerminal) Close() error {
|
||||||
|
if t.cmd == nil || t.cmd.Process == nil {
|
||||||
|
return fmt.Errorf("terminal process is already nil or not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取进程组 ID (PGID)。如果获取失败,则使用进程 PID 作为回退。
|
||||||
|
// 向进程组发送信号可以确保 shell 启动的子进程也能接收到信号。
|
||||||
pgid, err := syscall.Getpgid(t.cmd.Process.Pid)
|
pgid, err := syscall.Getpgid(t.cmd.Process.Pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("Failed to get process group ID for PID %d: %v. Using PID as PGID.\n", t.cmd.Process.Pid, err)
|
||||||
pgid = t.cmd.Process.Pid
|
pgid = t.cmd.Process.Pid
|
||||||
}
|
}
|
||||||
_ = syscall.Kill(-pgid, syscall.SIGTERM)
|
|
||||||
|
// 发送 SIGTERM 信号,请求进程组优雅退出
|
||||||
|
log.Printf("Sending SIGTERM to process group %d...\n", pgid)
|
||||||
|
_ = syscall.Kill(-pgid, syscall.SIGTERM) // -pgid 表示发送给进程组
|
||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
// 等待命令退出。如果命令已经退出,Wait()会立即返回。
|
||||||
done <- t.cmd.Wait()
|
done <- t.cmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
|
// 进程已退出
|
||||||
if err == nil {
|
if err == nil {
|
||||||
} else {
|
return nil
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.Exited() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("process group did not exit gracefully: %v", err)
|
|
||||||
}
|
}
|
||||||
return err
|
// 如果是 ExitError 且进程已退出,也视为成功关闭
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.Exited() {
|
||||||
|
return nil // 进程已退出,尽管可能不是0状态码,但我们认为它已关闭
|
||||||
|
}
|
||||||
|
return fmt.Errorf("process group did not exit gracefully: %v", err)
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
|
// 5 秒内未退出,发送 SIGKILL 强制终止
|
||||||
|
_ = syscall.Kill(-pgid, syscall.SIGKILL)
|
||||||
|
// 再次等待,确保进程被杀死,并获取最终的退出状态
|
||||||
|
killErr := <-done
|
||||||
|
if killErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if exitErr, ok := killErr.(*exec.ExitError); ok && exitErr.Exited() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Printf("Failed to kill process group %d after SIGKILL: %v\n", pgid, killErr)
|
||||||
|
return fmt.Errorf("failed to kill process group %d: %v", pgid, killErr)
|
||||||
}
|
}
|
||||||
// 超时未退出,强制 SIGKILL
|
|
||||||
_ = syscall.Kill(-pgid, syscall.SIGKILL)
|
|
||||||
return <-done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read 从伪终端读取数据。
|
||||||
func (t *unixTerminal) Read(p []byte) (int, error) {
|
func (t *unixTerminal) Read(p []byte) (int, error) {
|
||||||
|
if t.tty == nil {
|
||||||
|
return 0, fmt.Errorf("tty is nil")
|
||||||
|
}
|
||||||
return t.tty.Read(p)
|
return t.tty.Read(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write 向伪终端写入数据。
|
||||||
func (t *unixTerminal) Write(p []byte) (int, error) {
|
func (t *unixTerminal) Write(p []byte) (int, error) {
|
||||||
|
if t.tty == nil {
|
||||||
|
return 0, fmt.Errorf("tty is nil")
|
||||||
|
}
|
||||||
return t.tty.Write(p)
|
return t.tty.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resize 调整伪终端的大小。
|
||||||
func (t *unixTerminal) Resize(cols, rows int) error {
|
func (t *unixTerminal) Resize(cols, rows int) error {
|
||||||
|
if t.tty == nil {
|
||||||
|
return fmt.Errorf("tty is nil")
|
||||||
|
}
|
||||||
return pty.Setsize(t.tty, &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)})
|
return pty.Setsize(t.tty, &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait 等待 shell 进程退出。
|
||||||
func (t *unixTerminal) Wait() error {
|
func (t *unixTerminal) Wait() error {
|
||||||
|
if t.cmd == nil {
|
||||||
|
return fmt.Errorf("command is nil")
|
||||||
|
}
|
||||||
return t.cmd.Wait()
|
return t.cmd.Wait()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user