Golang TCP服务器子进程执行exit命令不退出且回显命令内容的问题
Golang TCP服务器子进程执行exit命令不退出且回显命令内容的问题
你遇到的这两个异常情况——输入命令后回显原命令+输出、exit命令无法终止子进程,本质都是PowerShell在非终端环境下的交互模式行为导致的,下面分别拆解原因并给出修复方案:
一、命令回显问题:原因与解决
为什么会回显命令?
PowerShell在交互模式下默认会开启输入回显。正常终端环境中,回显是由终端模拟器负责的,但当它的标准输入/输出绑定到TCP连接这种管道时,PowerShell会自己接管回显功能,所以你会看到输入的命令被重复输出。
修复方法
我们可以在启动PowerShell时,通过命令参数关闭它的回显功能,同时保留交互能力。把Go代码中启动PowerShell的那一行替换掉:
原代码:
cmd := exec.Command("powershell.exe")
替换为:
// 关闭回显,同时显式处理exit命令,保证子进程能正常退出 cmd := exec.Command("powershell.exe", "-Command", ` [Console]::EchoInput = $false; while ($true) { $inputLine = Read-Host; if ($inputLine -eq 'exit') { break; } try { iex $inputLine 2>&1 } catch { Write-Error $_ } } `)
这段PowerShell脚本的作用很明确:
- 第一行直接关闭输入回显
- 循环读取你的输入,一旦检测到
exit就终止循环(PowerShell随之退出) - 对其他命令,执行并把错误输出重定向到标准输出,和正常交互体验一致
二、exit命令无法终止子进程:原因与解决
为什么exit不生效?
默认情况下,当PowerShell的标准输入是TCP连接这种管道时,交互模式下的exit命令可能因为管道的特性,无法正确触发子进程退出;或者更准确地说,PowerShell执行exit后确实会退出,但socat因为没收到连接关闭的信号,会继续等待输入,导致你误以为子进程没退出。另外原Go代码里的panic也可能掩盖一些子进程退出的信号,导致连接关闭不及时。
修复方法
上面的PowerShell启动命令已经显式处理了exit命令,只要输入exit,PowerShell就会立即终止。同时我们可以优化Go代码的错误处理,避免panic导致服务器崩溃:
把原代码中的panic(err)替换为日志输出:
if err := cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "启动PowerShell失败: %s\n", err.Error()) return } if err := cmd.Wait(); err != nil { // 子进程非0退出码是正常情况(比如命令执行失败),不用崩溃 if exitErr, ok := err.(*exec.ExitError); ok { fmt.Fprintf(os.Stderr, "PowerShell退出,退出码: %d\n", exitErr.ExitCode()) } else { fmt.Fprintf(os.Stderr, "等待PowerShell退出时出错: %s\n", err.Error()) } }
完整修复后的Go代码
package main import ( "fmt" "net" "os" "os/exec" ) func handleConn(conn net.Conn) { defer conn.Close() // 关闭回显并显式处理exit命令的PowerShell启动参数 cmd := exec.Command("powershell.exe", "-Command", ` [Console]::EchoInput = $false; while ($true) { $inputLine = Read-Host; if ($inputLine -eq 'exit') { break; } try { iex $inputLine 2>&1 } catch { Write-Error $_ } } `) cmd.Stdin = conn cmd.Stdout = conn cmd.Stderr = conn if err := cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "启动PowerShell失败: %s\n", err.Error()) return } if err := cmd.Wait(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { fmt.Fprintf(os.Stderr, "PowerShell退出,退出码: %d\n", exitErr.ExitCode()) } else { fmt.Fprintf(os.Stderr, "等待PowerShell退出时出错: %s\n", err.Error()) } } fmt.Fprintf(os.Stderr, "连接已关闭\n") } func main() { // 创建监听套接字 ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Fprintf(os.Stderr, "发生错误: %s", err.Error()) return } // main函数返回时关闭监听 defer ln.Close() // 循环接受连接 for { conn, err := ln.Accept() if err != nil { fmt.Fprintf(os.Stderr, "接受连接出错: %s\n", err.Error()) continue } go handleConn(conn) } }
测试验证
修改代码后重新编译运行,再用socat连接:
socat tcp4:127.0.0.1:8080 -
此时你会发现:
- 输入命令后只会显示执行结果,不会再回显你输入的原命令
- 输入
exit后,PowerShell子进程会立即退出,TCP连接自动关闭,socat也会随之退出,再也不需要手动按Ctrl+C了
备注:内容来源于stack exchange,提问作者PSKP
相关产品推荐
相关产品推荐

