Fix console/window handling on windows

This commit is contained in:
Daniel 2019-07-16 13:27:31 +02:00
parent 1cc90f1eee
commit b30bb25ce3
5 changed files with 223 additions and 25 deletions

View file

@ -43,10 +43,32 @@ if [[ "$BUILD_SOURCE" == "" ]]; then
exit 1
fi
# build tools
EXTRA_LD_FLAGS=""
if [[ $GOOS == "windows" ]]; then
# checks
if [[ $CC_FOR_windows_amd64 == "" ]]; then
echo "ENV variable CC_FOR_windows_amd64 (c compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
exit 1
fi
if [[ $CXX_FOR_windows_amd64 == "" ]]; then
echo "ENV variable CXX_FOR_windows_amd64 (c++ compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
exit 1
fi
# compilers
export CC=$CC_FOR_windows_amd64
export CXX=$CXX_FOR_windows_amd64
# custom
export CGO_ENABLED=1
EXTRA_LD_FLAGS='-H windowsgui' # Hide console window by default (but we attach to parent console if available)
# generate resource.syso for windows metadata / icon
go generate
fi
echo "Please notice, that this build script includes metadata into the build."
echo "This information is useful for debugging and license compliance."
echo "Run the compiled binary with the -version flag to see the information included."
# build
BUILD_PATH="github.com/safing/portbase/info"
go build -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*
go build -ldflags "$EXTRA_LD_FLAGS -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*

10
pmctl/console_linux.go Normal file
View file

@ -0,0 +1,10 @@
package main
import "os/exec"
func attachToParentConsole() (attached bool, err error) {
return true, nil
}
func hideWindow(cmd *exec.Cmd) {
}

150
pmctl/console_windows.go Normal file
View file

@ -0,0 +1,150 @@
package main
// Parts of this file are FORKED
// from https://github.com/apenwarr/fixconsole/blob/35b2e7d921eb80a71a5f04f166ff0a1405bddf79/fixconsole_windows.go
// on 16.07.2019
// with Apache-2.0 license
// authored by https://github.com/apenwarr
// docs/sources:
// Stackoverflow Question: https://stackoverflow.com/questions/23743217/printing-output-to-a-command-window-when-golang-application-is-compiled-with-ld
// MS AttachConsole: https://docs.microsoft.com/en-us/windows/console/attachconsole
import (
"fmt"
"os"
"os/exec"
"syscall"
"golang.org/x/sys/windows"
)
const (
windowsAttachParentProcess = ^uintptr(0) // (DWORD)-1
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procAttachConsole = kernel32.NewProc("AttachConsole")
)
// Windows console output is a mess.
//
// If you compile as "-H windows", then if you launch your program without
// a console, Windows forcibly creates one to use as your stdin/stdout, which
// is silly for a GUI app, so we can't do that.
//
// If you compile as "-H windowsgui", then it doesn't create a console for
// your app... but also doesn't provide a working stdin/stdout/stderr even if
// you *did* launch from the console. However, you can use AttachConsole()
// to get a handle to your parent process's console, if any, and then
// os.NewFile() to turn that handle into a fd usable as stdout/stderr.
//
// However, then you have the problem that if you redirect stdout or stderr
// from the shell, you end up ignoring the redirection by forcing it to the
// console.
//
// To fix *that*, we have to detect whether there was a pre-existing stdout
// or not. We can check GetStdHandle(), which returns 0 for "should be
// console" and nonzero for "already pointing at a file."
//
// Be careful though! As soon as you run AttachConsole(), it resets *all*
// the GetStdHandle() handles to point them at the console instead, thus
// throwing away the original file redirects. So we have to GetStdHandle()
// *before* AttachConsole().
//
// For some reason, powershell redirections provide a valid file handle, but
// writing to that handle doesn't write to the file. I haven't found a way
// to work around that. (Windows 10.0.17763.379)
//
// Net result is as follows.
// Before:
// SHELL NON-REDIRECTED REDIRECTED
// explorer.exe no console n/a
// cmd.exe broken works
// powershell broken broken
// WSL bash broken works
// After
// SHELL NON-REDIRECTED REDIRECTED
// explorer.exe no console n/a
// cmd.exe works works
// powershell works broken
// WSL bash works works
//
// We don't seem to make anything worse, at least.
func attachToParentConsole() (attached bool, err error) {
// get std handles before we attempt to attach to parent console
stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
// attempt to attach to parent console
err = procAttachConsole.Find()
if err != nil {
return false, err
}
r1, _, err := procAttachConsole.Call(windowsAttachParentProcess)
if r1 == 0 {
// possible errors:
// ERROR_ACCESS_DENIED: already attached to console
// ERROR_INVALID_HANDLE: process does not have console
// ERROR_INVALID_PARAMETER: process does not exist
return false, nil
}
// get std handles after we attached to console
var invalid syscall.Handle
con := invalid
if stdin == invalid {
stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
}
if stdout == invalid {
stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
con = stdout
}
if stderr == invalid {
stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
con = stderr
}
// correct output mode
if con != invalid {
// Make sure the console is configured to convert
// \n to \r\n, like Go programs expect.
h := windows.Handle(con)
var st uint32
err := windows.GetConsoleMode(h, &st)
if err != nil {
fmt.Printf("%s failed to get console mode: %s\n", logPrefix, err)
} else {
err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN)
if err != nil {
fmt.Printf("%s failed to set console mode: %s\n", logPrefix, err)
}
}
}
// fix std handles to correct values (ie. redirects)
if stdin != invalid {
os.Stdin = os.NewFile(uintptr(stdin), "stdin")
fmt.Printf("%s fixed os.Stdin after attaching to parent console\n", logPrefix)
}
if stdout != invalid {
os.Stdout = os.NewFile(uintptr(stdout), "stdout")
fmt.Printf("%s fixed os.Stdout after attaching to parent console\n", logPrefix)
}
if stderr != invalid {
os.Stderr = os.NewFile(uintptr(stderr), "stderr")
fmt.Printf("%s fixed os.Stderr after attaching to parent console\n", logPrefix)
}
fmt.Printf("%s attached to parent console\n", logPrefix)
return true, nil
}
func hideWindow(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
}

View file

@ -2,13 +2,9 @@ package main
import (
"errors"
"flag"
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/safing/portbase/info"
"github.com/safing/portbase/log"
@ -35,6 +31,9 @@ var (
)
func init() {
// Let cobra ignore if we are running as "GUI" or not
cobra.MousetrapHelpText = ""
databaseRootDir = rootCmd.PersistentFlags().String("db", "", "set database directory")
err := rootCmd.MarkPersistentFlagRequired("db")
if err != nil {
@ -43,7 +42,14 @@ func init() {
}
func main() {
flag.Parse()
var err error
// check if we are running in a console (try to attach to parent console if available)
runningInConsole, err = attachToParentConsole()
if err != nil {
fmt.Printf("failed to attach to parent console: %s\n", err)
os.Exit(1)
}
// not using portbase logger
log.SetLogLevel(log.CriticalLevel)
@ -58,10 +64,10 @@ func main() {
// }()
// set meta info
info.Set("Portmaster Control", "0.2.1", "AGPLv3", true)
info.Set("Portmaster Control", "0.2.5", "AGPLv3", true)
// check if meta info is ok
err := info.CheckVersion()
err = info.CheckVersion()
if err != nil {
fmt.Printf("%s compile error: please compile using the provided build script\n", logPrefix)
os.Exit(1)
@ -73,14 +79,13 @@ func main() {
}
// start root command
if err := rootCmd.Execute(); err != nil {
if err = rootCmd.Execute(); err != nil {
os.Exit(1)
}
os.Exit(0)
}
func initPmCtl(cmd *cobra.Command, args []string) error {
func initPmCtl(cmd *cobra.Command, args []string) (err error) {
// transform from db base path to updates path
if *databaseRootDir != "" {
updates.SetDatabaseRoot(*databaseRootDir)

View file

@ -13,10 +13,16 @@ import (
"github.com/spf13/cobra"
)
var (
runningInConsole bool
onWindows = runtime.GOOS == "windows"
)
// Options for starting component
type Options struct {
Identifier string
AllowDownload bool
Identifier string
AllowDownload bool
AllowHidingWindow bool
}
func init() {
@ -36,8 +42,9 @@ var runCore = &cobra.Command{
Short: "Run the Portmaster Core",
RunE: func(cmd *cobra.Command, args []string) error {
return run(cmd, &Options{
Identifier: "core/portmaster-core",
AllowDownload: true,
Identifier: "core/portmaster-core",
AllowDownload: true,
AllowHidingWindow: true,
})
},
FParseErrWhitelist: cobra.FParseErrWhitelist{
@ -51,8 +58,9 @@ var runApp = &cobra.Command{
Short: "Run the Portmaster App",
RunE: func(cmd *cobra.Command, args []string) error {
return run(cmd, &Options{
Identifier: "app/portmaster-app",
AllowDownload: false,
Identifier: "app/portmaster-app",
AllowDownload: false,
AllowHidingWindow: false,
})
},
FParseErrWhitelist: cobra.FParseErrWhitelist{
@ -66,8 +74,9 @@ var runNotifier = &cobra.Command{
Short: "Run the Portmaster Notifier",
RunE: func(cmd *cobra.Command, args []string) error {
return run(cmd, &Options{
Identifier: "notifier/portmaster-notifier",
AllowDownload: false,
Identifier: "notifier/portmaster-notifier",
AllowDownload: false,
AllowHidingWindow: true,
})
},
FParseErrWhitelist: cobra.FParseErrWhitelist{
@ -86,7 +95,7 @@ func run(cmd *cobra.Command, opts *Options) error {
args = os.Args[3:]
// adapt identifier
if windows() {
if onWindows {
opts.Identifier += ".exe"
}
@ -98,7 +107,7 @@ func run(cmd *cobra.Command, opts *Options) error {
}
// check permission
if !windows() {
if !onWindows {
info, err := os.Stat(file.Path())
if err != nil {
return fmt.Errorf("failed to get file info on %s: %s", file.Path(), err)
@ -116,6 +125,12 @@ func run(cmd *cobra.Command, opts *Options) error {
// create command
exc := exec.Command(file.Path(), args...)
if !runningInConsole && opts.AllowHidingWindow {
// Windows only:
// only hide (all) windows of program if we are not running in console and windows may be hidden
hideWindow(exc)
}
// consume stdout/stderr
stdout, err := exc.StdoutPipe()
if err != nil {
@ -192,7 +207,3 @@ func run(cmd *cobra.Command, opts *Options) error {
fmt.Printf("%s %s completed successfully\n", logPrefix, opts.Identifier)
return nil
}
func windows() bool {
return runtime.GOOS == "windows"
}