From a8451fae658e7ea1f58127f0c1d37941867945f3 Mon Sep 17 00:00:00 2001 From: Branden Clark Date: Fri, 13 Jan 2023 11:53:34 -0500 Subject: [PATCH] Migrate systray to an fx.App (#14985) Deprecate single-dash args and add double-dash args Move code from cmd/systray to comp/systray Update UAC manifest to requireAdministrator Fix log file and add `system_tray.log_file` configuration option. --- .github/CODEOWNERS | 1 + cmd/systray/command/command.go | 103 +++++ cmd/systray/ddtray.exe.manifest | 2 +- cmd/systray/main_windows.go | 23 + cmd/systray/systray.go | 395 ----------------- comp/README.md | 10 + comp/systray/bundle.go | 11 + comp/systray/systray/component.go | 29 ++ {cmd => comp/systray}/systray/doconfigure.go | 28 +- {cmd => comp/systray}/systray/doflare.go | 51 ++- .../systray}/systray/doservicecontrol.go | 19 +- comp/systray/systray/systray.go | 413 ++++++++++++++++++ comp/systray/systray/uac.c | 84 ++++ comp/systray/systray/uac.h | 8 + omnibus/resources/agent/msi/source.wxs.erb | 4 +- omnibus/resources/iot/msi/source.wxs.erb | 4 +- pkg/config/config.go | 3 + pkg/config/config_template.yaml | 15 + pkg/util/winutil/users.go | 20 + ...-cmdline-args-compat-05fc525c6f6dea01.yaml | 13 + ...ire-admin-for-ddtray-5a087c08fc8d2f4c.yaml | 4 + tasks/systray.py | 18 +- 22 files changed, 799 insertions(+), 459 deletions(-) create mode 100644 cmd/systray/command/command.go create mode 100644 cmd/systray/main_windows.go delete mode 100644 cmd/systray/systray.go create mode 100644 comp/systray/bundle.go create mode 100644 comp/systray/systray/component.go rename {cmd => comp/systray}/systray/doconfigure.go (74%) rename {cmd => comp/systray}/systray/doflare.go (79%) rename {cmd => comp/systray}/systray/doservicecontrol.go (65%) create mode 100644 comp/systray/systray/systray.go create mode 100644 comp/systray/systray/uac.c create mode 100644 comp/systray/systray/uac.h create mode 100644 releasenotes/notes/break-ddtray-cmdline-args-compat-05fc525c6f6dea01.yaml create mode 100644 releasenotes/notes/require-admin-for-ddtray-5a087c08fc8d2f4c.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bc5b85f61d008..6d7bc7270a20a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -177,6 +177,7 @@ /comp @DataDog/agent-shared-components /comp/core @DataDog/agent-shared-components /comp/process @DataDog/processes +/comp/systray @DataDog/windows-agent # END COMPONENTS # pkg diff --git a/cmd/systray/command/command.go b/cmd/systray/command/command.go new file mode 100644 index 0000000000000..a793be32e76c3 --- /dev/null +++ b/cmd/systray/command/command.go @@ -0,0 +1,103 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. +//go:build windows +// +build windows + +// Package command implements the top-level `systray` binary, including its subcommands. +package command + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/DataDog/datadog-agent/cmd/agent/common" + "github.com/DataDog/datadog-agent/comp/core" + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/log" + "github.com/DataDog/datadog-agent/comp/systray/systray" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" + "github.com/DataDog/datadog-agent/pkg/util/winutil" + "go.uber.org/fx" +) + +const ( + defaultLogFile = "c:\\programdata\\datadog\\logs\\ddtray.log" +) + +var ( + // set by the build task and used to configure the logger to output to console when debugging. + // This value should correspond to the subsystem in the PE header. + // + // In normal circumstances, we don't want the systray to launch a console window when it runs + // so the default subsystem is "windows". However, console output can be useful for debugging. + // Console output can be viewd by setting the PE subsystem to "console". + subsystem = "windows" +) + +func init() { + // disable cobra mouse trap so cobra doesn't immediately kill our GUI app + cobra.MousetrapHelpText = "" +} + +// MakeCommand makes the top-level Cobra command for this app. +func MakeCommand() *cobra.Command { + systrayParams := systray.Params{} + + // log file path + var logFilePath string + confPath, err := winutil.GetProgramDataDir() + if err == nil { + logFilePath = filepath.Join(confPath, "logs", "ddtray.log") + } else { + logFilePath = defaultLogFile + } + + // log params + var logParams log.Params + if subsystem == "windows" { + logParams = log.LogForDaemon("TRAY", "system_tray.log_file", logFilePath) + } else if subsystem == "console" { + logParams = log.LogForOneShot("TRAY", "info", true) + } + + // root command + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s", os.Args[0]), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + return fxutil.Run( + // core + fx.Supply(core.BundleParams{ + ConfigParams: config.NewParams(common.DefaultConfPath), + LogParams: logParams, + }), + core.Bundle, + // systray + fx.Supply(systrayParams), + systray.Module, + // require the systray component, causing it to start + fx.Invoke(func(_ systray.Component) {}), + ) + }, + } + + // + // NOTE: The command line help/usage will not be visible in the release binary because the PE subsystem is "windows" + // + + cmd.PersistentFlags().BoolVar(&systrayParams.LaunchGuiFlag, "launch-gui", false, "Launch browser configuration and exit") + + // launch-elev=true only means the process should have been elevated so that it will not elevate again. If the + // parameter is specified but the process is not elevated, some operation will fail due to access denied. + cmd.PersistentFlags().BoolVar(&systrayParams.LaunchElevatedFlag, "launch-elev", false, "Launch program as elevated, internal use only") + + // If this parameter is specified, the process will try to carry out the command before the message loop. + cmd.PersistentFlags().StringVar(&systrayParams.LaunchCommand, "launch-cmd", "", "Carry out a specific command after launch") + + return cmd +} diff --git a/cmd/systray/ddtray.exe.manifest b/cmd/systray/ddtray.exe.manifest index 54d6cb1df9ea3..2cd320f3d310c 100644 --- a/cmd/systray/ddtray.exe.manifest +++ b/cmd/systray/ddtray.exe.manifest @@ -10,7 +10,7 @@ diff --git a/cmd/systray/main_windows.go b/cmd/systray/main_windows.go new file mode 100644 index 0000000000000..6dd2b9e8f6a75 --- /dev/null +++ b/cmd/systray/main_windows.go @@ -0,0 +1,23 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package main + +import ( + "os" + + "github.com/DataDog/datadog-agent/cmd/internal/runcmd" + "github.com/DataDog/datadog-agent/cmd/systray/command" + "github.com/DataDog/datadog-agent/pkg/util/log" +) + +func main() { + exitcode := 0 + defer func() { + log.Flush() + os.Exit(exitcode) + }() + exitcode = runcmd.Run(command.MakeCommand()) +} diff --git a/cmd/systray/systray.go b/cmd/systray/systray.go deleted file mode 100644 index 414b03b6a59eb..0000000000000 --- a/cmd/systray/systray.go +++ /dev/null @@ -1,395 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed -// under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2016-present Datadog, Inc. -//go:build windows -// +build windows - -package main - -//#include -// -//BOOL LaunchUnelevated(LPCWSTR CommandLine) -//{ -// BOOL result = FALSE; -// HWND hwnd = GetShellWindow(); -// -// if (hwnd != NULL) -// { -// DWORD pid; -// if (GetWindowThreadProcessId(hwnd, &pid) != 0) -// { -// HANDLE process = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid); -// -// if (process != NULL) -// { -// SIZE_T size; -// if ((!InitializeProcThreadAttributeList(NULL, 1, 0, &size)) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) -// { -// LPPROC_THREAD_ATTRIBUTE_LIST p = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size); -// if (p != NULL) -// { -// if (InitializeProcThreadAttributeList(p, 1, 0, &size)) -// { -// if (UpdateProcThreadAttribute(p, 0, -// PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, -// &process, sizeof(process), -// NULL, NULL)) -// { -// STARTUPINFOEXW siex = {0}; -// siex.lpAttributeList = p; -// siex.StartupInfo.cb = sizeof(siex); -// PROCESS_INFORMATION pi = {0}; -// -// size_t cmdlen = wcslen(CommandLine); -// size_t rawcmdlen = (cmdlen + 1) * sizeof(WCHAR); -// PWSTR cmdstr = (PWSTR)malloc(rawcmdlen); -// if (cmdstr != NULL) -// { -// memcpy(cmdstr, CommandLine, rawcmdlen); -// if (CreateProcessW(NULL, cmdstr, NULL, NULL, FALSE, -// CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT, -// NULL, NULL, &siex.StartupInfo, &pi)) -// { -// result = TRUE; -// CloseHandle(pi.hProcess); -// CloseHandle(pi.hThread); -// } -// free(cmdstr); -// } -// } -// } -// free(p); -// } -// } -// CloseHandle(process); -// } -// } -// } -// return result; -//} -import "C" - -import ( - "flag" - "fmt" - "os" - "os/exec" - "runtime" - "strings" - "time" - "unsafe" - - seelog "github.com/cihub/seelog" - - "github.com/DataDog/datadog-agent/pkg/util/log" - - "github.com/DataDog/datadog-agent/pkg/version" - - "github.com/lxn/walk" - "golang.org/x/sys/windows" -) - -type menuItem struct { - label string - handler walk.EventHandler - enabled bool -} - -const ( - cmdTextStartService = "StartService" - cmdTextStopService = "StopService" - cmdTextRestartService = "RestartService" - cmdTextConfig = "Config" -) - -var ( - separator = "SEPARATOR" - launchGraceTime = 2 - ni *walk.NotifyIcon - launchgui bool - launchelev bool - launchcmd string - eventname = windows.StringToUTF16Ptr("ddtray-event") - isUserAdmin bool - cmds = map[string]func(){ - cmdTextStartService: onStart, - cmdTextStopService: onStop, - cmdTextRestartService: onRestart, - cmdTextConfig: onConfigure, - } -) - -func init() { - enableLoggingToFile() - - isAdmin, err := isUserAnAdmin() - isUserAdmin = isAdmin - - if err != nil { - log.Warnf("Failed to call isUserAnAdmin %v", err) - // If we cannot determine if the user is admin or not let the user allow to click on the buttons. - isUserAdmin = true - } -} - -func createMenuItems(notifyIcon *walk.NotifyIcon) []menuItem { - av, _ := version.Agent() - verstring := av.GetNumberAndPre() - - menuHandler := func(cmd string) func() { - return func() { - execCmdOrElevate(cmd) - } - } - - menuitems := make([]menuItem, 0) - menuitems = append(menuitems, menuItem{label: verstring, enabled: false}) - menuitems = append(menuitems, menuItem{label: separator}) - menuitems = append(menuitems, menuItem{label: "&Start", handler: menuHandler(cmdTextStartService), enabled: true}) - menuitems = append(menuitems, menuItem{label: "S&top", handler: menuHandler(cmdTextStopService), enabled: true}) - menuitems = append(menuitems, menuItem{label: "&Restart", handler: menuHandler(cmdTextRestartService), enabled: true}) - menuitems = append(menuitems, menuItem{label: "&Configure", handler: menuHandler(cmdTextConfig), enabled: true}) - menuitems = append(menuitems, menuItem{label: "&Flare", handler: onFlare, enabled: true}) - menuitems = append(menuitems, menuItem{label: separator}) - menuitems = append(menuitems, menuItem{label: "E&xit", handler: onExit, enabled: true}) - - return menuitems -} - -func isUserAnAdmin() (bool, error) { - shell32 := windows.NewLazySystemDLL("Shell32.dll") - defer windows.FreeLibrary(windows.Handle(shell32.Handle())) - - isUserAnAdminProc := shell32.NewProc("IsUserAnAdmin") - ret, _, winError := isUserAnAdminProc.Call() - - if winError != windows.NTE_OP_OK { - return false, fmt.Errorf("IsUserAnAdmin returns error code %d", winError) - } - if ret == 0 { - return false, nil - } - return true, nil -} - -func showCustomMessage(notifyIcon *walk.NotifyIcon, message string) { - if err := notifyIcon.ShowCustom("Datadog Agent Manager", message, nil); err != nil { - log.Warnf("Failed to show custom message %v", err) - } -} - -func onExit() { - walk.App().Exit(0) -} - -func main() { - // Following https://github.com/lxn/win/commit/d9566253ae00d0a7dc7e4c9bda651dcfee029001 - // it's up to the caller to lock OS threads - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - flag.BoolVar(&launchgui, "launch-gui", false, "Launch browser configuration and exit") - - // launch-elev=true only means the process should have been elevated so that it will not elevate again. If the - // parameter is specified but the process is not elevated, some operation will fail due to access denied. - flag.BoolVar(&launchelev, "launch-elev", false, "Launch program as elevated, internal use only") - - // If this parameter is specified, the process will try to carry out the command before the message loop. - flag.StringVar(&launchcmd, "launch-cmd", "", "Carry out a specific command after launch") - flag.Parse() - - log.Debugf("launch-gui is %v, launch-elev is %v, launch-cmd is %v", launchgui, launchelev, launchcmd) - - if launchgui { - //enableLoggingToConsole() - defer log.Flush() - log.Debug("Preparing to launch configuration interface...") - onConfigure() - } - - // Check to see if the process is already running - h, _ := windows.OpenEvent(0x1F0003, // EVENT_ALL_ACCESS - false, - eventname) - - if h != windows.Handle(0) { - // Process already running. - windows.CloseHandle(h) - - // Wait a short period and recheck in case the other process will quit. - time.Sleep(time.Duration(launchGraceTime) * time.Second) - - // Try again - h, _ := windows.OpenEvent(0x1F0003, // EVENT_ALL_ACCESS - false, - eventname) - - if h != windows.Handle(0) { - windows.CloseHandle(h) - return - } - } - - // otherwise, create the handle so that nobody else will - h, _ = windows.CreateEvent(nil, 0, 0, eventname) - // should never fail; test just to make sure we don't close unopened handle - if h != windows.Handle(0) { - defer windows.CloseHandle(h) - } - // We need either a walk.MainWindow or a walk.Dialog for their message loop. - // We will not make it visible in this example, though. - mw, err := walk.NewMainWindow() - if err != nil { - log.Errorf("Failed to create main window %v", err) - os.Exit(1) - } - - // 1 is the ID of the MAIN_ICON in systray.rc - icon, err := walk.NewIconFromResourceId(1) - if err != nil { - log.Warnf("Failed to load icon %v", err) - } - // Create the notify icon and make sure we clean it up on exit. - ni, err = walk.NewNotifyIcon(mw) - if err != nil { - log.Errorf("Failed to create newNotifyIcon %v", err) - os.Exit(2) - } - defer ni.Dispose() - - // Set the icon and a tool tip text. - if err := ni.SetIcon(icon); err != nil { - log.Warnf("Failed to set icon %v", err) - } - if err := ni.SetToolTip("Click for info or use the context menu to exit."); err != nil { - log.Warnf("Failed to set tooltip text %v", err) - } - - // When the left mouse button is pressed, bring up our balloon. - ni.MouseDown().Attach(func(x, y int, button walk.MouseButton) { - if button != walk.LeftButton { - return - } - showCustomMessage(ni, "Please right click to display available options.") - }) - - menuitems := createMenuItems(ni) - - for _, item := range menuitems { - var action *walk.Action - if item.label == separator { - action = walk.NewSeparatorAction() - } else { - action = walk.NewAction() - if err := action.SetText(item.label); err != nil { - log.Warnf("Failed to set text for item %s %v", item.label, err) - continue - } - err = action.SetEnabled(item.enabled) - if err != nil { - log.Warnf("Failed to set enabled for item %s %v", item.label, err) - continue - } - if item.handler != nil { - _ = action.Triggered().Attach(item.handler) - } - } - err = ni.ContextMenu().Actions().Add(action) - if err != nil { - log.Warnf("Failed to add action for item %s to context menu %v", item.label, err) - continue - } - } - - // The notify icon is hidden initially, so we have to make it visible. - if err := ni.SetVisible(true); err != nil { - log.Warnf("Failed to set window visibility %v", err) - } - - // If a command is specified in process command line, carry it out. - if launchcmd != "" { - execCmdOrElevate(launchcmd) - } - - // Run the message loop. - mw.Run() -} - -// opens a browser window at the specified URL -func open(url string) error { - cmdptr := windows.StringToUTF16Ptr("rundll32.exe url.dll,FileProtocolHandler " + url) - if C.LaunchUnelevated(C.LPCWSTR(unsafe.Pointer(cmdptr))) == 0 { - // Failed to run process non-elevated, retry with normal launch. - log.Warnf("Failed to launch configuration page as non-elevated, will launch as current process.") - return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - } - - // Succeeded, return no error. - return nil -} - -func enableLoggingToFile() { - seeConfig := ` - - - - - ` - logger, _ := seelog.LoggerFromConfigAsBytes([]byte(seeConfig)) - log.ReplaceLogger(logger) -} - -//nolint:deadcode // for debugging -func enableLoggingToConsole() { - seeConfig := ` - - - - - ` - logger, _ := seelog.LoggerFromConfigAsBytes([]byte(seeConfig)) - log.ReplaceLogger(logger) -} - -// execCmdOrElevate carries out a command. If current process is not elevated and is not supposed to be elevated, it will launch -// itself as elevated and quit from the current instance. -func execCmdOrElevate(cmd string) { - if !launchelev && !isUserAdmin { - // If not launched as elevated and user is not admin, relaunch self. Use AND here to prevent from dead loop. - relaunchElevated(cmd) - - // If elevation failed, just quit to the caller. - return - } - - if cmds[cmd] != nil { - cmds[cmd]() - } -} - -// relaunchElevated launch another instance of the current process asking it to carry out a command as admin. -// If the function succeeds, it will quit the process, otherwise the function will return to the caller. -func relaunchElevated(cmd string) { - verb := "runas" - exe, _ := os.Executable() - cwd, _ := os.Getwd() - - // Reconstruct arguments, drop launch-gui and tell the new process it should have been elevated. - xargs := []string{"-launch-elev=true", "-launch-cmd=" + cmd} - args := strings.Join(xargs, " ") - - verbPtr, _ := windows.UTF16PtrFromString(verb) - exePtr, _ := windows.UTF16PtrFromString(exe) - cwdPtr, _ := windows.UTF16PtrFromString(cwd) - argPtr, _ := windows.UTF16PtrFromString(args) - - var showCmd int32 = 1 //SW_NORMAL - - err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd) - if err != nil { - log.Warnf("Failed to launch self as elevated %v", err) - } else { - onExit() - } -} diff --git a/comp/README.md b/comp/README.md index e1be85a271b4a..bbb2b04e33c4a 100644 --- a/comp/README.md +++ b/comp/README.md @@ -46,3 +46,13 @@ Package runner implements a component to run data collection checks in the Proce Package submitter implements a component to submit collected data in the Process Agent to supported Datadog intakes. + +## [comp/systray](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/systray) (Component Bundle) + +*Datadog Team*: windows-agent + +Package systray implements the Datadog Agent Manager System Tray + +### [comp/systray/systray](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/systray/systray) + +Package systray diff --git a/comp/systray/bundle.go b/comp/systray/bundle.go new file mode 100644 index 0000000000000..64ad9d8fb9b24 --- /dev/null +++ b/comp/systray/bundle.go @@ -0,0 +1,11 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. +//go:build windows +// +build windows + +// Package systray implements the Datadog Agent Manager System Tray +package systray + +// team: windows-agent diff --git a/comp/systray/systray/component.go b/comp/systray/systray/component.go new file mode 100644 index 0000000000000..93c34786c545d --- /dev/null +++ b/comp/systray/systray/component.go @@ -0,0 +1,29 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. +//go:build windows +// +build windows + +// Package systray +package systray + +import ( + "github.com/DataDog/datadog-agent/pkg/util/fxutil" + "go.uber.org/fx" +) + +// team: windows-agent + +type Params struct { + LaunchGuiFlag bool + LaunchElevatedFlag bool + LaunchCommand string +} + +type Component interface { +} + +var Module = fxutil.Component( + fx.Provide(newSystray), +) diff --git a/cmd/systray/doconfigure.go b/comp/systray/systray/doconfigure.go similarity index 74% rename from cmd/systray/doconfigure.go rename to comp/systray/systray/doconfigure.go index 98fea32629680..c6321e10ae986 100644 --- a/cmd/systray/doconfigure.go +++ b/comp/systray/systray/doconfigure.go @@ -5,37 +5,29 @@ //go:build windows // +build windows -package main +package systray import ( "encoding/json" "fmt" - "github.com/DataDog/datadog-agent/cmd/agent/common" "github.com/DataDog/datadog-agent/pkg/api/security" "github.com/DataDog/datadog-agent/pkg/api/util" - "github.com/DataDog/datadog-agent/pkg/config" - - "github.com/DataDog/datadog-agent/pkg/util/log" + pkgconfig "github.com/DataDog/datadog-agent/pkg/config" ) -func onConfigure() { +func onConfigure(s *systray) { // seems like a waste. However, the handler function doesn't expect an error code. // this just eats the error code. - err := doConfigure() + err := doConfigure(s) if err != nil { - log.Warnf("Failed to launch gui %v", err) + s.log.Warnf("Failed to launch gui %v", err) } return } -func doConfigure() error { - - err := common.SetupConfigWithoutSecrets("", "") - if err != nil { - return fmt.Errorf("unable to set up global agent configuration: %v", err) - } +func doConfigure(s *systray) error { - guiPort := config.Datadog.GetString("GUI_port") + guiPort := s.config.GetString("GUI_port") if guiPort == "-1" { return fmt.Errorf("GUI not enabled: to enable, please set an appropriate port in your datadog.yaml file") } @@ -48,11 +40,11 @@ func doConfigure() error { // Get the CSRF token from the agent c := util.GetClient(false) // FIX: get certificates right then make this true - ipcAddress, err := config.GetIPCAddress() + ipcAddress, err := pkgconfig.GetIPCAddress() if err != nil { return err } - urlstr := fmt.Sprintf("https://%v:%v/agent/gui/csrf-token", ipcAddress, config.Datadog.GetInt("cmd_port")) + urlstr := fmt.Sprintf("https://%v:%v/agent/gui/csrf-token", ipcAddress, s.config.GetInt("cmd_port")) err = util.SetAuthToken() if err != nil { return err @@ -74,6 +66,6 @@ func doConfigure() error { return fmt.Errorf("error opening GUI: " + err.Error()) } - log.Debugf("GUI opened at 127.0.0.1:" + guiPort + "\n") + s.log.Debugf("GUI opened at 127.0.0.1:" + guiPort + "\n") return nil } diff --git a/cmd/systray/doflare.go b/comp/systray/systray/doflare.go similarity index 79% rename from cmd/systray/doflare.go rename to comp/systray/systray/doflare.go index 33cb5b7dad195..d8c145d880e39 100644 --- a/cmd/systray/doflare.go +++ b/comp/systray/systray/doflare.go @@ -5,7 +5,7 @@ //go:build windows // +build windows -package main +package systray import ( "bytes" @@ -21,8 +21,8 @@ import ( "github.com/DataDog/datadog-agent/cmd/agent/common" "github.com/DataDog/datadog-agent/pkg/api/util" "github.com/DataDog/datadog-agent/pkg/config" - "github.com/DataDog/datadog-agent/pkg/flare" - "github.com/DataDog/datadog-agent/pkg/util/log" + pkgflare "github.com/DataDog/datadog-agent/pkg/flare" + pkglog "github.com/DataDog/datadog-agent/pkg/util/log" ) const ( @@ -83,11 +83,11 @@ func dialogProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) (result uintp x, y, _, _ := calcPos(wndrect, dlgrect) r, _, err = procSetWindowPos.Call(uintptr(hwnd), 0, uintptr(x), uintptr(y), uintptr(0), uintptr(0), uintptr(0x0041)) if r != 0 { - log.Debugf("failed to set window pos %v", err) + pkglog.Debugf("failed to set window pos %v", err) } } } else { - log.Debugf("failed to get pos %v", err) + pkglog.Debugf("failed to get pos %v", err) } } // set the "OK" to disabled until there's something approximating an email @@ -122,11 +122,11 @@ func dialogProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) (result uintp win.SendDlgItemMessage(hwnd, IDC_TICKET_EDIT, win.WM_GETTEXT, 255, uintptr(unsafe.Pointer(&buf[0]))) info.caseid = windows.UTF16ToString(buf) - log.Debugf("ticket number %s", info.caseid) + pkglog.Debugf("ticket number %s", info.caseid) win.SendDlgItemMessage(hwnd, IDC_EMAIL_EDIT, win.WM_GETTEXT, 255, uintptr(unsafe.Pointer(&buf[0]))) info.email = windows.UTF16ToString(buf) - log.Debugf("email %s", info.email) + pkglog.Debugf("email %s", info.email) win.EndDialog(hwnd, win.IDOK) return uintptr(1) @@ -137,21 +137,22 @@ func dialogProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) (result uintp } return uintptr(0) } -func onFlare() { +func onFlare(s *systray) { + // Create a dialog box to prompt for ticket number and email, then create and submit the flare // library will allow multiple calls (multi-threaded window proc?) // however, we're using a single instance of the info structure to // pass data around. Don't allow multiple dialogs to be displayed // (in go1.18, this could be done with sync.Mutex#TryLock) if !inProgress.CAS(false, true) { - log.Warn("Dialog already in progress, skipping") + s.log.Warn("Dialog already in progress, skipping") return } defer inProgress.Store(false) myInst := win.GetModuleHandle(nil) if myInst == win.HINSTANCE(0) { - log.Errorf("Failed to get my own module handle") + s.log.Errorf("Failed to get my own module handle") return } ret := win.DialogBoxParam(myInst, win.MAKEINTRESOURCE(uintptr(IDD_DIALOG1)), win.HWND(0), windows.NewCallback(dialogProc), uintptr(0)) @@ -161,7 +162,7 @@ func onFlare() { // got a non number, just create a new case info.caseid = "0" } - r, e := requestFlare(info.caseid, info.email) + r, e := requestFlare(s, info.caseid, info.email) caption, _ := windows.UTF16PtrFromString("Datadog Flare") var text *uint16 if e == nil { @@ -171,17 +172,14 @@ func onFlare() { } win.MessageBox(win.HWND(0), text, caption, win.MB_OK) } - log.Debugf("DialogBoxParam returns %d", ret) + s.log.Debugf("DialogBoxParam returns %d", ret) } -func requestFlare(caseID, customerEmail string) (response string, e error) { - log.Debug("Asking the agent to build the flare archive.") +func requestFlare(s *systray, caseID, customerEmail string) (response string, e error) { + // For first try, ask the agent to build the flare + s.log.Debug("Asking the agent to build the flare archive.") - e = common.SetupConfig("") - if e != nil { - return - } c := util.GetClient(false) // FIX: get certificates right then make this true ipcAddress, err := config.GetIPCAddress() if err != nil { @@ -208,28 +206,29 @@ func requestFlare(caseID, customerEmail string) (response string, e error) { r, e := util.DoPost(c, urlstr, "application/json", bytes.NewBuffer([]byte{})) var filePath string if e != nil { + // The agent could not make the flare, try create one from this context if r != nil && string(r) != "" { - log.Warnf("The agent ran into an error while making the flare: %s\n", r) + s.log.Warnf("The agent ran into an error while making the flare: %s\n", r) e = fmt.Errorf("Error getting flare from running agent: %s", r) } else { - log.Debug("The agent was unable to make the flare.") + s.log.Debug("The agent was unable to make the flare.") e = fmt.Errorf("Error getting flare from running agent: %w", e) } - log.Debug("Initiating flare locally.") + s.log.Debug("Initiating flare locally.") - filePath, e = flare.CreateArchive(true, common.GetDistPath(), common.PyChecksPath, []string{logFile, jmxLogFile}, nil, e) + filePath, e = s.flare.Create(true, common.GetDistPath(), common.PyChecksPath, []string{logFile, jmxLogFile}, nil, e) if e != nil { - log.Errorf("The flare zipfile failed to be created: %s\n", e) + s.log.Errorf("The flare zipfile failed to be created: %s\n", e) return } } else { filePath = string(r) } - log.Warnf("%s is going to be uploaded to Datadog\n", filePath) + s.log.Warnf("%s is going to be uploaded to Datadog\n", filePath) - response, e = flare.SendFlare(filePath, caseID, customerEmail) - log.Debug(response) + response, e = pkgflare.SendFlare(filePath, caseID, customerEmail) + s.log.Debug(response) if e != nil { return } diff --git a/cmd/systray/doservicecontrol.go b/comp/systray/systray/doservicecontrol.go similarity index 65% rename from cmd/systray/doservicecontrol.go rename to comp/systray/systray/doservicecontrol.go index 3915d981a46ff..a6e6323ef423b 100644 --- a/cmd/systray/doservicecontrol.go +++ b/comp/systray/systray/doservicecontrol.go @@ -6,29 +6,26 @@ //go:build windows // +build windows -package main +package systray import ( "github.com/DataDog/datadog-agent/cmd/agent/windows/controlsvc" - "github.com/DataDog/datadog-agent/pkg/util/log" ) -func onRestart() { +func onRestart(s *systray) { if err := controlsvc.RestartService(); err != nil { - log.Warnf("Failed to restart datadog service %v", err) + s.log.Warnf("Failed to restart datadog service %v", err) } - } -func onStart() { + +func onStart(s *systray) { if err := controlsvc.StartService(); err != nil { - log.Warnf("Failed to start datadog service %v", err) + s.log.Warnf("Failed to start datadog service %v", err) } - } -func onStop() { +func onStop(s *systray) { if err := controlsvc.StopService(); err != nil { - log.Warnf("Failed to stop datadog service %v", err) + s.log.Warnf("Failed to stop datadog service %v", err) } - } diff --git a/comp/systray/systray/systray.go b/comp/systray/systray/systray.go new file mode 100644 index 0000000000000..8f809082a76db --- /dev/null +++ b/comp/systray/systray/systray.go @@ -0,0 +1,413 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. +//go:build windows +// +build windows + +package systray + +//#include "uac.h" +import "C" + +import ( + "context" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "sync" + "time" + "unsafe" + + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/flare" + "github.com/DataDog/datadog-agent/comp/core/log" + pkglog "github.com/DataDog/datadog-agent/pkg/util/log" + "github.com/DataDog/datadog-agent/pkg/util/winutil" + "github.com/DataDog/datadog-agent/pkg/version" + + "github.com/lxn/walk" + "github.com/lxn/win" + "go.uber.org/fx" + "golang.org/x/sys/windows" +) + +type dependencies struct { + fx.In + + Lc fx.Lifecycle + Shutdowner fx.Shutdowner + + Log log.Component + Config config.Component + Flare flare.Component + Params Params +} + +type systray struct { + // For triggering Shutdown + shutdowner fx.Shutdowner + + log log.Component + config config.Component + flare flare.Component + params Params + + isUserAdmin bool + + // allocated in start, destroyed in stop + singletonEventHandle windows.Handle + + // Window management + notifyWindowToStop func() + routineWaitGroup sync.WaitGroup +} + +type menuItem struct { + label string + handler walk.EventHandler + enabled bool +} + +const ( + launchGraceTime = 2 + eventname = "ddtray-event" + cmdTextStartService = "StartService" + cmdTextStopService = "StopService" + cmdTextRestartService = "RestartService" + cmdTextConfig = "Config" + menuSeparator = "SEPARATOR" +) + +var ( + cmds = map[string]func(*systray){ + cmdTextStartService: onStart, + cmdTextStopService: onStop, + cmdTextRestartService: onRestart, + cmdTextConfig: onConfigure, + } +) + +// newSystray creates a new systray component, which will start and stop based on +// the fx Lifecycle +func newSystray(deps dependencies) Component { + // fx init + s := &systray{ + log: deps.Log, + config: deps.Config, + flare: deps.Flare, + params: deps.Params, + shutdowner: deps.Shutdowner, + } + + // fx lifecycle hooks + deps.Lc.Append(fx.Hook{OnStart: s.start, OnStop: s.stop}) + + // init vars + isAdmin, err := winutil.IsUserAnAdmin() + if err != nil { + s.log.Warnf("Failed to call IsUserAnAdmin %v", err) + // If we cannot determine if the user is admin or not let the user allow to click on the buttons. + s.isUserAdmin = true + } else { + s.isUserAdmin = isAdmin + } + + return s +} + +// start hook has a fx enforced timeout, so don't do long running things +func (s *systray) start(ctx context.Context) error { + var err error + + s.log.Debugf("launch-gui is %v, launch-elev is %v, launch-cmd is %v", s.params.LaunchGuiFlag, s.params.LaunchElevatedFlag, s.params.LaunchCommand) + + if s.params.LaunchGuiFlag { + s.log.Debug("Preparing to launch configuration interface...") + go onConfigure(s) + } + + s.singletonEventHandle, err = acquireProcessSingleton(eventname) + if err != nil { + s.log.Errorf("Failed to acquire singleton %v", err) + return err + } + + s.routineWaitGroup.Add(1) + go windowRoutine(s) + + // If a command is specified in process command line, carry it out. + if s.params.LaunchCommand != "" { + go execCmdOrElevate(s, s.params.LaunchCommand) + } + + return nil +} + +func (s *systray) stop(ctx context.Context) error { + if s.notifyWindowToStop != nil { + // Send stop message to window (stops windowRoutine goroutine) + s.notifyWindowToStop() + } + + // wait for goroutine to finish + s.routineWaitGroup.Wait() + + // release our singleton + if s.singletonEventHandle != windows.Handle(0) { + windows.CloseHandle(s.singletonEventHandle) + } + + return nil +} + +// Run window setup and message loop in a single threadlocked goroutine +// https://github.com/lxn/walk/issues/601 +// Use the notifyWindowToStop function to stop the message loop +// Use routineWaitGroup to wait until the routine exits +func windowRoutine(s *systray) { + // Following https://github.com/lxn/win/commit/d9566253ae00d0a7dc7e4c9bda651dcfee029001 + // it's up to the caller to lock OS threads + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + defer s.routineWaitGroup.Done() + + // We need either a walk.MainWindow or a walk.Dialog for their message loop. + mw, err := walk.NewMainWindow() + if err != nil { + s.log.Errorf("Failed to create main window %v", err) + return + } + defer mw.Dispose() + + ni, err := createNotifyIcon(s, mw) + if err != nil { + s.log.Errorf("Failed to create notification tray icon %v", err) + return + } + defer ni.Dispose() + + // Provide a function that will trigger this thread to run PostQuitMessage() + // which will cause the message loop to return + s.notifyWindowToStop = func() { + mw.Synchronize(func() { + win.PostQuitMessage(0) + }) + } + + // Run the message loop + // use the notifyWindowToStop function to stop the message loop + mw.Run() +} + +func acquireProcessSingleton(eventname string) (windows.Handle, error) { + var utf16EventName = windows.StringToUTF16Ptr(eventname) + + // Check to see if the process is already running + h, _ := windows.OpenEvent(windows.EVENT_ALL_ACCESS, + false, + utf16EventName) + + if h != windows.Handle(0) { + // Process already running. + windows.CloseHandle(h) + + // Wait a short period and recheck in case the other process will quit. + time.Sleep(time.Duration(launchGraceTime) * time.Second) + + // Try again + h, _ := windows.OpenEvent(windows.EVENT_ALL_ACCESS, + false, + utf16EventName) + + if h != windows.Handle(0) { + windows.CloseHandle(h) + return windows.Handle(0), fmt.Errorf("systray is already running") + } + } + + // otherwise, create the handle so that nobody else will + h, err := windows.CreateEvent(nil, 0, 0, utf16EventName) + if err != nil { + // can fail with ERROR_ALREADY_EXISTS if we lost a race + if h != windows.Handle(0) { + windows.CloseHandle(h) + } + return windows.Handle(0), err + } + + return h, nil +} + +// this function must be called from and the NotifyIcon used from a single thread locked goroutine +// https://github.com/lxn/walk/issues/601 +func createNotifyIcon(s *systray, mw *walk.MainWindow) (ni *walk.NotifyIcon, err error) { + // Create the notify icon (must be cleaned up) + ni, err = walk.NewNotifyIcon(mw) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + ni.Dispose() + ni = nil + } + }() + + // Set the icon and a tool tip text. + // 1 is the ID of the MAIN_ICON in systray.rc + icon, err := walk.NewIconFromResourceId(1) + if err != nil { + s.log.Warnf("Failed to load icon %v", err) + } + if err := ni.SetIcon(icon); err != nil { + s.log.Warnf("Failed to set icon %v", err) + } + + // Set mouseover tooltip + if err := ni.SetToolTip("Click for info or use the context menu to exit."); err != nil { + s.log.Warnf("Failed to set tooltip text %v", err) + } + + // When the left mouse button is pressed, bring up our balloon. + ni.MouseDown().Attach(func(x, y int, button walk.MouseButton) { + if button != walk.LeftButton { + return + } + showCustomMessage(ni, "Please right click to display available options.") + }) + + menuitems := createMenuItems(s, ni) + + for _, item := range menuitems { + var action *walk.Action + if item.label == menuSeparator { + action = walk.NewSeparatorAction() + } else { + action = walk.NewAction() + if err := action.SetText(item.label); err != nil { + s.log.Warnf("Failed to set text for item %s %v", item.label, err) + continue + } + err = action.SetEnabled(item.enabled) + if err != nil { + s.log.Warnf("Failed to set enabled for item %s %v", item.label, err) + continue + } + if item.handler != nil { + _ = action.Triggered().Attach(item.handler) + } + } + err = ni.ContextMenu().Actions().Add(action) + if err != nil { + s.log.Warnf("Failed to add action for item %s to context menu %v", item.label, err) + continue + } + } + + // The notify icon is hidden initially, so we have to make it visible. + if err := ni.SetVisible(true); err != nil { + s.log.Warnf("Failed to set window visibility %v", err) + } + + return ni, nil +} + +func showCustomMessage(notifyIcon *walk.NotifyIcon, message string) { + if err := notifyIcon.ShowCustom("Datadog Agent Manager", message, nil); err != nil { + pkglog.Warnf("Failed to show custom message %v", err) + } +} + +func triggerShutdown(s *systray) { + if s != nil { + // Tell fx to begin shutdown process + s.shutdowner.Shutdown() + } +} + +func onExit(s *systray) { + triggerShutdown(s) +} + +func createMenuItems(s *systray, notifyIcon *walk.NotifyIcon) []menuItem { + av, _ := version.Agent() + verstring := av.GetNumberAndPre() + + menuHandler := func(cmd string) func() { + return func() { + execCmdOrElevate(s, cmd) + } + } + + menuitems := make([]menuItem, 0) + menuitems = append(menuitems, menuItem{label: verstring, enabled: false}) + menuitems = append(menuitems, menuItem{label: menuSeparator}) + menuitems = append(menuitems, menuItem{label: "&Start", handler: menuHandler(cmdTextStartService), enabled: true}) + menuitems = append(menuitems, menuItem{label: "S&top", handler: menuHandler(cmdTextStopService), enabled: true}) + menuitems = append(menuitems, menuItem{label: "&Restart", handler: menuHandler(cmdTextRestartService), enabled: true}) + menuitems = append(menuitems, menuItem{label: "&Configure", handler: menuHandler(cmdTextConfig), enabled: true}) + menuitems = append(menuitems, menuItem{label: "&Flare", handler: func() { onFlare(s) }, enabled: true}) + menuitems = append(menuitems, menuItem{label: menuSeparator}) + menuitems = append(menuitems, menuItem{label: "E&xit", handler: func() { onExit(s) }, enabled: true}) + + return menuitems +} + +// opens a browser window at the specified URL +func open(url string) error { + cmdptr := windows.StringToUTF16Ptr("rundll32.exe url.dll,FileProtocolHandler " + url) + if C.LaunchUnelevated(C.LPCWSTR(unsafe.Pointer(cmdptr))) == 0 { + // Failed to run process non-elevated, retry with normal launch. + pkglog.Warnf("Failed to launch configuration page as non-elevated, will launch as current process.") + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + } + + // Succeeded, return no error. + return nil +} + +// execCmdOrElevate carries out a command. If current process is not elevated and is not supposed to be elevated, it will launch +// itself as elevated and quit from the current instance. +func execCmdOrElevate(s *systray, cmd string) { + if !s.params.LaunchElevatedFlag && !s.isUserAdmin { + // If not launched as elevated and user is not admin, relaunch self. Use AND here to prevent from dead loop. + relaunchElevated(s, cmd) + + // If elevation failed, just quit to the caller. + return + } + + if cmds[cmd] != nil { + cmds[cmd](s) + } +} + +// relaunchElevated launch another instance of the current process asking it to carry out a command as admin. +// If the function succeeds, it will quit the process, otherwise the function will return to the caller. +func relaunchElevated(s *systray, cmd string) { + verb := "runas" + exe, _ := os.Executable() + cwd, _ := os.Getwd() + + // Reconstruct arguments, drop launch-gui and tell the new process it should have been elevated. + xargs := []string{"--launch-elev=true", "--launch-cmd=" + cmd} + args := strings.Join(xargs, " ") + + verbPtr, _ := windows.UTF16PtrFromString(verb) + exePtr, _ := windows.UTF16PtrFromString(exe) + cwdPtr, _ := windows.UTF16PtrFromString(cwd) + argPtr, _ := windows.UTF16PtrFromString(args) + + var showCmd int32 = 1 //SW_NORMAL + + err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd) + if err != nil { + s.log.Warnf("Failed to launch self as elevated %v", err) + } else { + triggerShutdown(s) + } +} diff --git a/comp/systray/systray/uac.c b/comp/systray/systray/uac.c new file mode 100644 index 0000000000000..4f90235728a5e --- /dev/null +++ b/comp/systray/systray/uac.c @@ -0,0 +1,84 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +#include "uac.h" + +// Attempts to drop privileges from an elevated process by creating a new process and +// setting the parent process to be the user's explorer.exe. This causes the new process to +// inherit its access token from explorer.exe. +// +// Technique relies on having permission to open explorer.exe with PROCESS_CREATE_PROCESS, +// this access is verified against the explorer.exe process DACL. +// Generally, +// If the current process was elevated via a consent prompt, the user account is the same and access will be granted. +// If the current process was elevated via a credential prompt, the user account is different and access will be denied. +// https://learn.microsoft.com/en-us/windows/security/identity-protection/user-account-control/how-user-account-control-works +// +// TODO: Try to enable SeDebugPrivilege. This will allow this function to support credential prompts +// if group policy has not been modified to remove SeDebugPrivilege from Administrators. +BOOL LaunchUnelevated(LPCWSTR CommandLine) +{ + BOOL result = FALSE; + // Get handle to the Shell's desktop window + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getshellwindow + HWND hwnd = GetShellWindow(); + + if (hwnd != NULL) + { + DWORD pid; + // Get pid that created the window, this should be PID of explorer.exe + if (GetWindowThreadProcessId(hwnd, &pid) != 0) + { + HANDLE process = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid); + + if (process != NULL) + { + // To set the parent process, create a thread attribute list containing PROC_THREAD_ATTRIBUTE_PARENT_PROCESS + SIZE_T size; + if ((!InitializeProcThreadAttributeList(NULL, 1, 0, &size)) && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) + { + LPPROC_THREAD_ATTRIBUTE_LIST p = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size); + if (p != NULL) + { + if (InitializeProcThreadAttributeList(p, 1, 0, &size)) + { + if (UpdateProcThreadAttribute(p, 0, + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, + &process, sizeof(process), + NULL, NULL)) + { + STARTUPINFOEXW siex = {0}; + siex.lpAttributeList = p; + siex.StartupInfo.cb = sizeof(siex); + PROCESS_INFORMATION pi = {0}; + + size_t cmdlen = wcslen(CommandLine); + size_t rawcmdlen = (cmdlen + 1) * sizeof(WCHAR); + PWSTR cmdstr = (PWSTR)malloc(rawcmdlen); + if (cmdstr != NULL) + { + memcpy(cmdstr, CommandLine, rawcmdlen); + if (CreateProcessW(NULL, cmdstr, NULL, NULL, FALSE, + CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT, + NULL, NULL, &siex.StartupInfo, &pi)) + { + result = TRUE; + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + free(cmdstr); + } + } + } + DeleteProcThreadAttributeList(p); + free(p); + } + } + CloseHandle(process); + } + } + } + return result; +} diff --git a/comp/systray/systray/uac.h b/comp/systray/systray/uac.h new file mode 100644 index 0000000000000..5b591464c2eb1 --- /dev/null +++ b/comp/systray/systray/uac.h @@ -0,0 +1,8 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +#include + +BOOL LaunchUnelevated(LPCWSTR CommandLine); diff --git a/omnibus/resources/agent/msi/source.wxs.erb b/omnibus/resources/agent/msi/source.wxs.erb index d9b6d66688cbc..ec5a09da30637 100644 --- a/omnibus/resources/agent/msi/source.wxs.erb +++ b/omnibus/resources/agent/msi/source.wxs.erb @@ -186,7 +186,7 @@ Directory="AGENT" Execute="immediate" Return="asyncNoWait" - ExeCommand=""[AGENT]ddtray.exe" "-launch-gui"" /> + ExeCommand=""[AGENT]ddtray.exe" "--launch-gui"" /> @@ -367,7 +367,7 @@ Name="Datadog Agent Manager" Description="Manage your Datadog Agent" Target="[AGENT]ddtray.exe" - Arguments= ""-launch-gui"" + Arguments= ""--launch-gui"" WorkingDirectory="AGENT"/> diff --git a/omnibus/resources/iot/msi/source.wxs.erb b/omnibus/resources/iot/msi/source.wxs.erb index 1218d4a75fb4b..1e75203e02657 100644 --- a/omnibus/resources/iot/msi/source.wxs.erb +++ b/omnibus/resources/iot/msi/source.wxs.erb @@ -109,7 +109,7 @@ Directory="AGENT" Execute="immediate" Return="asyncNoWait" - ExeCommand=""[AGENT]ddtray.exe" "-launch-gui"" /> + ExeCommand=""[AGENT]ddtray.exe" "--launch-gui"" /> @@ -291,7 +291,7 @@ Name="Datadog Agent Manager" Description="Manage your Datadog Agent" Target="[AGENT]ddtray.exe" - Arguments= ""-launch-gui"" + Arguments= ""--launch-gui"" WorkingDirectory="AGENT"/> diff --git a/pkg/config/config.go b/pkg/config/config.go index b431d10055b79..15392dad10f87 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1237,6 +1237,9 @@ func InitConfig(config Config) { bindVectorOptions(config, Metrics) bindVectorOptions(config, Logs) + // Datadog Agent Manager System Tray + config.BindEnvAndSetDefault("system_tray.log_file", "") + setupAPM(config) SetupOTLP(config) setupProcesses(config) diff --git a/pkg/config/config_template.yaml b/pkg/config/config_template.yaml index 4f8a589e32a16..72aa9df42b678 100644 --- a/pkg/config/config_template.yaml +++ b/pkg/config/config_template.yaml @@ -3450,3 +3450,18 @@ api_key: # # verbosity: normal {{end -}} +{{- if (eq .OS "windows")}} +##################################################### +## Datadog Agent Manager System Tray Configuration ## +##################################################### + +## @param system_tray - custom object - optional +## This section configures the Datadog Agent Manager System Tray +# +# system_tray: + ## @param log_file - string - optional - default: %ProgramData%\Datadog\logs\ddtray.log + ## @env DD_TRAY_LOG_FILE - string - optional + ## The full path to the file where Datadog Agent Manager System Tray logs are written. + # + # log_file: +{{end -}} diff --git a/pkg/util/winutil/users.go b/pkg/util/winutil/users.go index 51a89437cf6ff..b04fe6908f269 100644 --- a/pkg/util/winutil/users.go +++ b/pkg/util/winutil/users.go @@ -8,6 +8,7 @@ package winutil import ( + "fmt" "syscall" "golang.org/x/sys/windows" @@ -40,3 +41,22 @@ func GetSidFromUser() (*windows.SID, error) { return windows.StringToSid(sidString) } + +// Returns true is a user is a member of the Administrator's group +// TODO: Microsoft does not recommend using this function, instead CheckTokenMembership should be used. +// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-isuseranadmin +func IsUserAnAdmin() (bool, error) { + shell32 := windows.NewLazySystemDLL("Shell32.dll") + defer windows.FreeLibrary(windows.Handle(shell32.Handle())) + + isUserAnAdminProc := shell32.NewProc("IsUserAnAdmin") + ret, _, winError := isUserAnAdminProc.Call() + + if winError != windows.NTE_OP_OK { + return false, fmt.Errorf("IsUserAnAdmin returns error code %d", winError) + } + if ret == 0 { + return false, nil + } + return true, nil +} diff --git a/releasenotes/notes/break-ddtray-cmdline-args-compat-05fc525c6f6dea01.yaml b/releasenotes/notes/break-ddtray-cmdline-args-compat-05fc525c6f6dea01.yaml new file mode 100644 index 0000000000000..befe9e7b413f7 --- /dev/null +++ b/releasenotes/notes/break-ddtray-cmdline-args-compat-05fc525c6f6dea01.yaml @@ -0,0 +1,13 @@ +--- +upgrade: + - | + The command line arguments to the Datadog Agent Manager for Windows ``ddtray.exe`` + have changed from single-dash arguments to double-dash arguments. + For example, ``-launch-gui`` must now be provided as ``--launch-gui``. + The start menu shortcut created by the installer will be automatically updated. + Any custom scripts or shortcuts that launch ``ddtray.exe`` with arguments must be updated manually. +deprecations: + - | + The command line arguments to the Datadog Agent Manager for Windows ``ddtray.exe`` + have changed from single-dash arguments to double-dash arguments. + For example, ``-launch-gui`` must now be provided as ``--launch-gui``. diff --git a/releasenotes/notes/require-admin-for-ddtray-5a087c08fc8d2f4c.yaml b/releasenotes/notes/require-admin-for-ddtray-5a087c08fc8d2f4c.yaml new file mode 100644 index 0000000000000..45f7e02f54834 --- /dev/null +++ b/releasenotes/notes/require-admin-for-ddtray-5a087c08fc8d2f4c.yaml @@ -0,0 +1,4 @@ +--- +other: + - | + The Datadog Agent Manager ``ddtray.exe`` now requires admin to launch. diff --git a/tasks/systray.py b/tasks/systray.py index d1bd2bf57a5ee..98c1d898014b7 100644 --- a/tasks/systray.py +++ b/tasks/systray.py @@ -16,7 +16,7 @@ @task -def build(ctx, rebuild=False, race=False, major_version='7', arch="x64", go_mod="mod"): +def build(ctx, debug=False, console=False, rebuild=False, race=False, major_version='7', arch="x64", go_mod="mod"): """ Build the agent. If the bits to include in the build are not specified, the values from `invoke.yaml` will be used. @@ -44,7 +44,14 @@ def build(ctx, rebuild=False, race=False, major_version='7', arch="x64", go_mod= command += "-i cmd/systray/systray.rc -O coff -o cmd/systray/rsrc.syso" ctx.run(command) ldflags = get_version_ldflags(ctx, major_version=major_version) - ldflags += "-s -w -linkmode external -extldflags '-Wl,--subsystem,windows' " + if not debug: + ldflags += "-s -w " + if console: + subsystem = 'console' + else: + subsystem = 'windows' + ldflags += f"-X {REPO_PATH}/cmd/systray/command/command.subsystem={subsystem} " + ldflags += f"-linkmode external -extldflags '-Wl,--subsystem,{subsystem}' " cmd = "go build -mod={go_mod} {race_opt} {build_type} -o {agent_bin} -ldflags=\"{ldflags}\" {REPO_PATH}/cmd/systray" args = { "go_mod": go_mod, @@ -68,7 +75,7 @@ def run(ctx, rebuild=False, race=False, skip_build=False): if not skip_build: build(ctx, rebuild, race) - ctx.run(os.path.join(BIN_PATH, bin_name("ddtray.exe"))) + ctx.run(os.path.join(BIN_PATH, bin_name("ddtray"))) @task @@ -82,4 +89,7 @@ def clean(ctx): # remove the bin/agent folder print("Remove systray executable") - ctx.run("rm -rf ./bin/agent/ddtray.exe") + try: + os.remove(os.path.join(BIN_PATH, bin_name("ddtray"))) + except Exception as e: + print(e)