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)