/* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; ** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ** See the GNU Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ package zbxcmd import ( "bytes" "errors" "fmt" "os/exec" "path/filepath" "strings" "syscall" "time" "unsafe" "golang.org/x/sys/windows" "golang.zabbix.com/sdk/log" ) type process struct { Pid int Handle uintptr } var ( ntResumeProcess *syscall.Proc cmd_path string ) func execute(s string, timeout time.Duration, path string, strict bool) (out string, err error) { if cmd_path == "" { cmd_exe := "cmd.exe" if cmd_exe, err = exec.LookPath(cmd_exe); err != nil && !errors.Is(err, exec.ErrDot) { return "", fmt.Errorf("Cannot find path to %s command: %s", cmd_exe, err) } if cmd_path, err = filepath.Abs(cmd_exe); err != nil { return "", fmt.Errorf("Cannot find full path to %s command: %s", cmd_exe, err) } } cmd := exec.Command(cmd_path) cmd.Dir = path var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b var job windows.Handle if job, err = windows.CreateJobObject(nil, nil); err != nil { return } defer windows.CloseHandle(job) cmd.SysProcAttr = &windows.SysProcAttr{ CreationFlags: windows.CREATE_SUSPENDED, CmdLine: fmt.Sprintf(`/C "%s"`, s), } if err = cmd.Start(); err != nil { return "", fmt.Errorf("Cannot execute command (%s, path: %s): %s", s, path, err) } processHandle := windows.Handle((*process)(unsafe.Pointer(cmd.Process)).Handle) defer func() { if cmd.ProcessState == nil { log.Warningf("attempting to terminate process because normal processing was interrupted by error: %s", err) windows.TerminateProcess(processHandle, 0) _ = cmd.Wait() } }() if err = windows.AssignProcessToJobObject(job, processHandle); err != nil { log.Warningf("cannot assign process to a job: %s", err) return } t := time.AfterFunc(timeout, func() { if err = windows.TerminateJobObject(job, 0); err != nil { log.Warningf("failed to kill [%s]: %s", s, err) } }) var rc uintptr if rc, _, err = ntResumeProcess.Call(uintptr(processHandle)); int32(rc) < 0 { log.Warningf("cannot resume process: %s", err) return } werr := cmd.Wait() if !t.Stop() { return "", fmt.Errorf("Timeout while executing a shell script.") } // we need to check error after t.Stop so we can inform the user if timeout was reached and Zabbix agent2 terminated the command if strict && werr != nil { return "", fmt.Errorf("Command execution failed: %s", werr) } if MaxExecuteOutputLenB <= len(b.String()) { return "", fmt.Errorf("Command output exceeded limit of %d KB", MaxExecuteOutputLenB/1024) } return strings.TrimRight(b.String(), " \t\r\n"), nil } func ExecuteBackground(s string) (err error) { if cmd_path == "" { cmd_exe := "cmd.exe" if cmd_exe, err = exec.LookPath(cmd_exe); err != nil && !errors.Is(err, exec.ErrDot) { return fmt.Errorf("Cannot find path to %s command: %s", cmd_exe, err) } if cmd_path, err = filepath.Abs(cmd_exe); err != nil { return fmt.Errorf("Cannot find full path to %s command: %s", cmd_exe, err) } } cmd := exec.Command(cmd_path) cmd.SysProcAttr = &windows.SysProcAttr{ CmdLine: fmt.Sprintf(`/C "%s"`, s), } if err = cmd.Start(); err != nil { return fmt.Errorf("Cannot execute command (%s): %s", s, err) } go func() { _ = cmd.Wait() }() return nil } func init() { dll := syscall.MustLoadDLL("ntdll.dll") defer dll.Release() ntResumeProcess = dll.MustFindProc("NtResumeProcess") }