Golang中os/exec在Windows下的StdinPipe问题解析

Golang中os/exec在Windows下的StdinPipe问题解析

您使用的是哪个Go版本(go version)?

1.18.2

这个问题在最新版本中是否复现?

package main

import (
	"fmt"
	"io"
	"log"
	"os/exec"
	"os"
)

func main() {
	command :="openconnect"
	param := []string{"https://192.168.0.100:1443","-u","test","--passwd-on-stdin"}
	cmd :=exec.Command(command,param...)
	stdin, err := cmd.StdinPipe()
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		defer stdin.Close()
		io.WriteString(stdin, "123\n")
	}()
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err = cmd.Start(); err != nil { //Use start, not run
		fmt.Println("An error occured: ", err) //replace with logger, or anything you want
	}
	cmd.Wait()

}
  • StdinPipe 不工作

您使用的是哪个操作系统和处理器架构(go env)?

windows

您做了什么?

#19452

我遇到了同样的问题。我的代码与作者的代码基本相同。

  1. 对于用golang编写的命令行程序,stdinpipe可以正常工作。
  2. BeamMP-Server.exe,stdinpipe无法正常工作,但如果我手动启动它,我可以正常输入命令。 https://beammp.com/ 下载服务器

您期望看到什么?

stdinpipe 对 BeamMP-Server.exe 程序正常工作。

您实际看到了什么?

  • 没有错误
  • 命令不工作
  • 程序的标准输出没有打印所有内容

更多关于Golang中os/exec在Windows下的StdinPipe问题解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中os/exec在Windows下的StdinPipe问题解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Windows下使用os/exec.StdinPipe()确实存在一些特殊问题,特别是与某些控制台应用程序交互时。主要原因是Windows控制台程序的输入处理方式与Unix-like系统不同。

问题分析

Windows控制台程序通常期望从控制台输入缓冲区读取,而不是标准输入管道。当使用StdinPipe()时,程序可能无法正确接收输入。

解决方案

方法1:使用syscall设置输入模式(推荐)

package main

import (
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "syscall"
    "unsafe"
)

const (
    ENABLE_ECHO_INPUT      = 0x0004
    ENABLE_LINE_INPUT      = 0x0002
    ENABLE_PROCESSED_INPUT = 0x0001
    ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
)

func setConsoleMode(h uintptr, mode uint32) error {
    kernel32 := syscall.NewLazyDLL("kernel32.dll")
    setConsoleModeProc := kernel32.NewProc("SetConsoleMode")
    
    r, _, err := setConsoleModeProc.Call(h, uintptr(mode))
    if r == 0 {
        return err
    }
    return nil
}

func main() {
    command := "openconnect"
    param := []string{"https://192.168.0.100:1443", "-u", "test", "--passwd-on-stdin"}
    cmd := exec.Command(command, param...)
    
    // 为Windows设置额外的属性
    cmd.SysProcAttr = &syscall.SysProcAttr{
        CreationFlags: syscall.CREATE_NEW_CONSOLE,
    }
    
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }
    
    go func() {
        defer stdin.Close()
        // 尝试多次写入,确保数据被接收
        io.WriteString(stdin, "123\n")
        io.WriteString(stdin, "\n") // 额外的换行确保输入被处理
    }()
    
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    
    if err = cmd.Start(); err != nil {
        fmt.Println("启动错误: ", err)
    }
    
    cmd.Wait()
}

方法2:使用匿名管道和手动处理

package main

import (
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "syscall"
    "time"
)

func main() {
    command := "BeamMP-Server.exe"
    param := []string{}
    cmd := exec.Command(command, param...)
    
    // 创建匿名管道
    stdinPipe, stdinWriter, err := os.Pipe()
    if err != nil {
        log.Fatal(err)
    }
    
    cmd.Stdin = stdinPipe
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    
    // 设置控制台属性
    cmd.SysProcAttr = &syscall.SysProcAttr{
        CreationFlags: syscall.CREATE_NEW_CONSOLE,
    }
    
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }
    
    // 等待程序启动
    time.Sleep(2 * time.Second)
    
    // 写入数据
    _, err = io.WriteString(stdinWriter, "123\n")
    if err != nil {
        log.Printf("写入错误: %v", err)
    }
    
    // 关闭写入端
    stdinWriter.Close()
    
    cmd.Wait()
}

方法3:使用conpty虚拟终端(Windows 10+)

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "time"
    "github.com/ActiveState/termtest/conpty"
)

func main() {
    command := "BeamMP-Server.exe"
    
    // 使用conpty创建虚拟终端
    cpty, err := conpty.New(80, 25)
    if err != nil {
        log.Fatal(err)
    }
    defer cpty.Close()
    
    // 启动进程
    cmd := exec.Command(command)
    pid, err := cpty.Spawn(cmd.Path, cmd.Args, &syscall.ProcAttr{})
    if err != nil {
        log.Fatal(err)
    }
    
    // 读取输出
    go func() {
        buf := make([]byte, 1024)
        for {
            n, err := cpty.OutPipe().Read(buf)
            if err != nil {
                return
            }
            os.Stdout.Write(buf[:n])
        }
    }()
    
    // 等待并发送输入
    time.Sleep(2 * time.Second)
    
    // 发送密码
    _, err = cpty.Write([]byte("123\n"))
    if err != nil {
        log.Printf("写入错误: %v", err)
    }
    
    // 等待进程结束
    process, _ := os.FindProcess(pid)
    process.Wait()
}

方法4:使用go-ansi库处理Windows控制台

package main

import (
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "github.com/k0kubun/go-ansi"
)

func main() {
    command := "BeamMP-Server.exe"
    cmd := exec.Command(command)
    
    // 使用ansi包装器
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }
    
    // 创建ANSI兼容的写入器
    ansiWriter := ansi.NewAnsiStdin(stdin)
    
    go func() {
        defer stdin.Close()
        // 使用ANSI序列确保输入被处理
        io.WriteString(ansiWriter, "123\r\n")
    }()
    
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    
    if err := cmd.Start(); err != nil {
        fmt.Println("启动错误:", err)
    }
    
    cmd.Wait()
}

关键点

  1. Windows控制台程序:许多Windows控制台程序期望从控制台输入缓冲区读取,而不是标准输入管道
  2. 换行符处理:Windows使用\r\n,而Unix使用\n
  3. 缓冲问题:可能需要刷新缓冲区或发送额外字符
  4. 程序类型:检查目标程序是GUI程序还是真正的控制台程序

对于BeamMP-Server.exe这类程序,建议使用方法1或方法2,并确保:

  • 使用syscall.CREATE_NEW_CONSOLE标志
  • 正确处理换行符(\r\n
  • 添加适当的延迟确保程序已初始化完成
回到顶部