使用 -ldflags -H=windowsgui 编译 golang 应用程序时将输出打印到命令窗口

Posted

技术标签:

【中文标题】使用 -ldflags -H=windowsgui 编译 golang 应用程序时将输出打印到命令窗口【英文标题】:Printing output to a command window when golang application is compiled with -ldflags -H=windowsgui 【发布时间】:2014-05-19 16:56:16 【问题描述】:

我有一个通常在后台静默运行的应用程序,所以我用它来编译它

go build -ldflags -H=windowsgui <gofile>

要在命令行检查版本,我想将 -V 标志传递给命令行,以获取保存要打印到命令提示符的版本的字符串,然后让应用程序退出。我添加了标志包和代码。当我用

测试它时
go run <gofile> -V

...它可以很好地打印版本。当我编译 exe 时,它​​只是退出,什么也不打印。我怀疑这是编译标志导致它无法访问控制台并将我的文本发送到位桶。

我尝试了使用 println、fprintf 和 os.stderr.write 打印到 stderr 和 stdout 的变体,但编译后的应用程序中没有显示任何内容。使用这些标志编译时,我应该如何尝试将字符串打印到命令提示符?

【问题讨论】:

【参考方案1】:

问题在于,当使用在其PE header 中设置为“Windows”的the "subsystem" variable 的可执行文件创建进程时,该进程的three standard handles 关闭并且它不与任何控制台相关联——不管你是否从控制台运行它。 (事实上​​,如果您从控制台运行一个其子系统设置为“console”not 的可执行文件,则会为该进程强制创建一个控制台并将该进程附加到它上——您通常会看到它作为一个突然弹出的控制台窗口。)

因此,要从 Windows 上的 GUI 进程向控制台打印任何内容,您必须将该进程显式连接到附加到其父进程(如果有的话)的控制台,例如 here 解释的那样。为此,您调用AttachConsole API 函数。使用 Go,这可以使用 syscall 包来完成:

package main

import (
    "fmt"
    "syscall"
)

const (
    ATTACH_PARENT_PROCESS = ^uint32(0) // (DWORD)-1
)

var (
    modkernel32 = syscall.NewLazyDLL("kernel32.dll")

    procAttachConsole = modkernel32.NewProc("AttachConsole")

)

func AttachConsole(dwParentProcess uint32) (ok bool) 
    r0, _, _ := syscall.Syscall(procAttachConsole.Addr(), 1, uintptr(dwParentProcess), 0, 0)
    ok = bool(r0 != 0)
    return


func main() 
    ok := AttachConsole(ATTACH_PARENT_PROCESS)
    if ok 
        fmt.Println("Okay, attached")
    

要真正完整,当AttachConsole() 失败时,这段代码可能应该采用以下两条路线之一:

调用AllocConsole() 为其创建自己的控制台窗口。

可以说这对于显示版本信息几乎没有用,因为该过程通常在打印后退出,由此产生的用户体验将是一个控制台窗口弹出并立即消失;高级用户会得到提示,他们应该从控制台重新运行应用程序,但普通人可能无法应对。

发布一个显示相同信息的 GUI 对话框。

我认为这正是我们所需要的:请注意,显示帮助/使用消息以响应用户指定一些命令行参数通常与控制台在心理上相关,但这不是要遵循的教条:例如,尝试在控制台上运行 msiexec.exe /? 看看会发生什么。

【讨论】:

【参考方案2】:

这里已经发布的解决方案的一个问题是它们将所有输出重定向到控制台,所以如果我运行./myprogram &gt;file,到file的重定向会丢失。我编写了一个新模块 github.com/apenwarr/fixconsole,可以避免这个问题。你可以这样使用它:

import (
        "fmt"
        "github.com/apenwarr/fixconsole"
        "os"
)

func main() 
        err := fixconsole.FixConsoleIfNeeded()
        if err != nil 
                fmt.Fatalf("FixConsoleOutput: %v\n", err)
        
        os.Stdout.WriteString(fmt.Sprintf("Hello stdout\n"))
        os.Stderr.WriteString(fmt.Sprintf("Hello stderr\n"))

【讨论】:

【参考方案3】:

上面的答案很有帮助,但可惜它对我来说开箱即用。经过一些额外的研究,我来到了这段代码:

// go build -ldflags -H=windowsgui
package main

import "fmt"
import "os"
import "syscall"

func main() 
    modkernel32 := syscall.NewLazyDLL("kernel32.dll")
    procAllocConsole := modkernel32.NewProc("AllocConsole")
    r0, r1, err0 := syscall.Syscall(procAllocConsole.Addr(), 0, 0, 0, 0)
    if r0 == 0  // Allocation failed, probably process already has a console
        fmt.Printf("Could not allocate console: %s. Check build flags..", err0)
        os.Exit(1)
    
    hout, err1 := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
    hin, err2 := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
    if err1 != nil || err2 != nil  // nowhere to print the error
        os.Exit(2)
    
    os.Stdout = os.NewFile(uintptr(hout), "/dev/stdout")
    os.Stdin = os.NewFile(uintptr(hin), "/dev/stdin")
    fmt.Printf("Hello!\nResult of console allocation: ")
    fmt.Printf("r0=%d,r1=%d,err=%s\nFor Goodbye press Enter..", r0, r1, err0)
    var s string
    fmt.Scanln(&s)
    os.Exit(0)

关键点:分配/附加控制台后,需要获取stdout句柄,使用该句柄打开文件并将其分配给os.Stdout变量。如果您需要标准输入,您必须对标准输入重复相同的操作。

【讨论】:

【参考方案4】:

您可以在不使用 -H=windowsgui; 的情况下获得所需的行为;您基本上会创建一个标准应用程序(带有自己的控制台窗口),并隐藏它直到程序退出。

func Console(show bool) 
    var getWin = syscall.NewLazyDLL("kernel32.dll").NewProc("GetConsoleWindow")
    var showWin = syscall.NewLazyDLL("user32.dll").NewProc("ShowWindow")
    hwnd, _, _ := getWin.Call()
    if hwnd == 0 
            return
    
    if show 
       var SW_RESTORE uintptr = 9
       showWin.Call(hwnd, SW_RESTORE)
     else 
       var SW_HIDE uintptr = 0
       showWin.Call(hwnd, SW_HIDE)
    

然后像这样使用它:

func main() 
    Console(false)
    defer Console(true)
    ...
    fmt.Println("Hello World")
    ...

【讨论】:

【参考方案5】:

如果您构建一个无窗口应用程序,您可以使用 PowerShell 命令 Out-String 获得输出

.\\main.exe | out-string

您的构建命令可能如下所示:

cls; go build -i -ldflags -H=windowsgui main.go; .\\main.exe | out-string;

cls; go run -ldflags -H=windowsgui main.go | out-string

不需要复杂的系统调用或内核 DLL!

【讨论】:

以上是关于使用 -ldflags -H=windowsgui 编译 golang 应用程序时将输出打印到命令窗口的主要内容,如果未能解决你的问题,请参考以下文章

WindowsGUI自动化测试框架搭建-详细设计&框架设计

WindowsGUI自动化测试框架搭建-登录模块封装

WindowsGUI自动化测试框架搭建-截图功能封装和调用

WindowsGUI自动化测试框架搭建(十五)-框架README.md设计

WindowsGUI自动化测试框架搭建-邮件服务模块封装(sendmail)

WindowsGUI自动化测试框架搭建-环境部署