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

huangapple go评论73阅读模式
英文:

Printing output to a command window when golang application is compiled with -ldflags -H=windowsgui

问题

我有一个通常在后台静默运行的应用程序,所以我使用以下命令进行编译:

go build -ldflags -H=windowsgui <gofile>

为了在命令行中检查版本,我想要传递一个 -V 标志给命令行,以便将包含版本信息的字符串打印到命令提示符,然后退出应用程序。我添加了 flag 包和相应的代码。当我用以下命令测试时:

go run <gofile> -V

...它可以正常打印版本信息。但是当我编译成可执行文件后,它只是退出了,没有打印任何内容。我怀疑是编译标志导致它无法访问控制台,将我的文本丢失。

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

英文:

I have an application that usually runs silent in the background, so I compile it with

go build -ldflags -H=windowsgui &lt;gofile&gt;

To check the version at the command line, I wanted to pass a -V flag to the command line to get the string holding the version to be printed to the command prompt then have the application exit. I added the flag package and code. When I test it with

go run &lt;gofile&gt; -V

...it prints the version fine. When I compile the exe, it just exits, printing nothing. I suspect it's the compilation flag causing it to not access the console and sending my text into the bit bucket.

I've tried variations to print to stderr and stdout, using println and fprintf and os.stderr.write, but nothing appears from the compiled application. How should I try printing a string to the command prompt when compiled with those flags?

答案1

得分: 23

问题是,当使用一个可执行文件创建一个进程时,该可执行文件的PE头中的"subsystem"变量设置为"Windows",该进程的三个标准句柄将被关闭,并且它不与任何控制台相关联,无论你是否从控制台运行它。(实际上,如果你从控制台运行一个将子系统设置为"console"的可执行文件,控制台将被强制创建,并且进程将附加到它上面,你通常会突然看到一个控制台窗口弹出。)

因此,在Windows上从GUI进程打印任何内容到控制台,你必须显式地将该进程连接到与其父进程(如果有)相关联的控制台,就像这里解释的那样。为了做到这一点,你可以调用"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 /?",看看会发生什么。

英文:

The problem is that when a process is created using an executable which has the "subsystem" variable in its PE header set to "Windows", the process has its three standard handles closed and it is not associated with any console&mdash;no matter if you run it from the console or not. (In fact, if you run an executable which has its subsystem set to "console" not from a console, a console is forcibly created for that process and the process is attached to it&mdash;you usually see it as a console window popping up all of a sudden.)

Hence, to print anything to the console from a GUI process on Windows you have to explicitly connect that process to the console which is attached to its parent process (if it has one), like explained here for instance. To do this, you call the AttachConsole API function. With Go, this can be done using the syscall package:

package main

import (
	&quot;fmt&quot;
	&quot;syscall&quot;
)

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

var (
	modkernel32 = syscall.NewLazyDLL(&quot;kernel32.dll&quot;)

	procAttachConsole = modkernel32.NewProc(&quot;AttachConsole&quot;)

)

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(&quot;Okay, attached&quot;)
	}
}

To be truly complete, when AttachConsole() fails, this code should probably take one of these two routes:

  • Call AllocConsole() to get its own console window created for it.

    It'd say this is pretty much useless for displaying version information as the process usually quits after printing it, and the resulting user experience will be a console window popping up and immediately disappearing; power users will get a hint that they should re-run the application from the console but mere mortals won't probably cope.

  • Post a GUI dialog displaying the same information.

    I think this is just what's needed: note that displaying help/usage messages in response to the user specifying some command-line argument is quite often mentally associated with the console, but this is not a dogma to follow: for instance, try running msiexec.exe /? at the console and see what happens.

答案2

得分: 4

这里已经发布的解决方案存在一个问题,即它们将所有输出重定向到控制台,所以如果我运行 ./myprogram >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"))
}
英文:

One problem with the solutions already posted here is that they redirect all output to the console, so if I run ./myprogram &gt;file, the redirection to file gets lost. I've written a new module, github.com/apenwarr/fixconsole, that avoids this problem. You can use it like this:

import (
        &quot;fmt&quot;
        &quot;github.com/apenwarr/fixconsole&quot;
        &quot;os&quot;
)

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

答案3

得分: 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 { // 分配失败,可能进程已经有控制台
        fmt.Printf("无法分配控制台:%s。请检查构建标志..", 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 { // 无法打印错误信息
        os.Exit(2)
    }
    os.Stdout = os.NewFile(uintptr(hout), "/dev/stdout")
    os.Stdin = os.NewFile(uintptr(hin), "/dev/stdin")
    fmt.Printf("你好!\n控制台分配结果:")
    fmt.Printf("r0=%d,r1=%d,err=%s\n按Enter键退出..", r0, r1, err0)
    var s string
    fmt.Scanln(&s)
    os.Exit(0)
}

关键点:在分配/附加控制台之后,需要获取stdout句柄,使用该句柄打开文件,并将其赋值给os.Stdout变量。如果需要stdin,需要重复相同的步骤获取stdin句柄。

英文:

Answer above was helpful but alas it did not work for me out of the box. After some additional research I came to this code:

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

import &quot;fmt&quot;
import &quot;os&quot;
import &quot;syscall&quot;

func main() {
	modkernel32 := syscall.NewLazyDLL(&quot;kernel32.dll&quot;)
	procAllocConsole := modkernel32.NewProc(&quot;AllocConsole&quot;)
	r0, r1, err0 := syscall.Syscall(procAllocConsole.Addr(), 0, 0, 0, 0)
	if r0 == 0 { // Allocation failed, probably process already has a console
		fmt.Printf(&quot;Could not allocate console: %s. Check build flags..&quot;, 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), &quot;/dev/stdout&quot;)
	os.Stdin = os.NewFile(uintptr(hin), &quot;/dev/stdin&quot;)
	fmt.Printf(&quot;Hello!\nResult of console allocation: &quot;)
	fmt.Printf(&quot;r0=%d,r1=%d,err=%s\nFor Goodbye press Enter..&quot;, r0, r1, err0)
	var s string
	fmt.Scanln(&amp;s)
	os.Exit(0)
}

The key point: after allocating/attaching the console, there is need to get stdout handle, open file using this handle and assign it to os.Stdout variable. If you need stdin you have to repeat the same for stdin.

答案4

得分: 2

你可以在不使用-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")
    ...
}

然后像这样使用它:

英文:

You can get the desired behavior without using -H=windowsgui; you'd basically create a standard app (with its own console window), and hide it until the program exits.

func Console(show bool) {
    var getWin = syscall.NewLazyDLL(&quot;kernel32.dll&quot;).NewProc(&quot;GetConsoleWindow&quot;)
    var showWin = syscall.NewLazyDLL(&quot;user32.dll&quot;).NewProc(&quot;ShowWindow&quot;)
    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)
    }
}

And then use it like this:

func main() {
    Console(false)
    defer Console(true)
    ...
    fmt.Println(&quot;Hello World&quot;)
    ...
}

答案5

得分: 0

如果你构建一个没有窗口的应用程序,你可以使用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!

英文:

If you build a windowless app you can get output with PowerShell command Out-String

.\\main.exe | out-string

your build command may look like:

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

or

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

No tricky syscalls nor kernel DLLs needed!

huangapple
  • 本文由 发表于 2014年5月20日 00:56:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/23743217.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定