diff --git a/pmctl/build b/pmctl/build index a311b43b..26fb3525 100755 --- a/pmctl/build +++ b/pmctl/build @@ -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}" $* diff --git a/pmctl/console_linux.go b/pmctl/console_linux.go new file mode 100644 index 00000000..471e100a --- /dev/null +++ b/pmctl/console_linux.go @@ -0,0 +1,10 @@ +package main + +import "os/exec" + +func attachToParentConsole() (attached bool, err error) { + return true, nil +} + +func hideWindow(cmd *exec.Cmd) { +} diff --git a/pmctl/console_windows.go b/pmctl/console_windows.go new file mode 100644 index 00000000..ba9a3ac0 --- /dev/null +++ b/pmctl/console_windows.go @@ -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, + } +} diff --git a/pmctl/main.go b/pmctl/main.go index 169ca59b..f7950f3a 100644 --- a/pmctl/main.go +++ b/pmctl/main.go @@ -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) diff --git a/pmctl/run.go b/pmctl/run.go index 054d13ef..6f817067 100644 --- a/pmctl/run.go +++ b/pmctl/run.go @@ -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" -}