Skip to content

Commit db2781a

Browse files
For .NET capture, the yc-dot-net.exe file was build on 64-bit machine. Due to that if the user is running target process on 32-bit machine then the thread dump and heap data capture was failing. To fix this now generating 2 versions of yc-dot-net.exe file for 32-bit and 64-bit. Enhanced the yc-360 script to automatically read this file based on the OS version.
1 parent e62b2e5 commit db2781a

3 files changed

Lines changed: 171 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build !windows
2+
3+
package capture
4+
5+
func resolveDotnetToolByPid(pid int) (string, bool, error) {
6+
return "", false, nil
7+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//go:build windows
2+
3+
package capture
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
11+
"golang.org/x/sys/windows"
12+
"yc-agent/internal/config"
13+
)
14+
15+
const (
16+
dotnetToolX86 = "yc-dot-net-x86.exe"
17+
dotnetToolX64 = "yc-dot-net-x64.exe"
18+
)
19+
20+
// resolveDotnetToolByPid picks helper binary by target process architecture.
21+
// Returns (toolPath, true, nil) when selection succeeded.
22+
// Returns ("", false, nil) when no architecture-specific binary was found.
23+
func resolveDotnetToolByPid(pid int) (string, bool, error) {
24+
targetArch, err := detectWindowsProcessArch(pid)
25+
if err != nil {
26+
return "", false, nil // non-fatal: caller will use default resolver
27+
}
28+
29+
preferred := dotnetToolX64
30+
if targetArch == "x86" {
31+
preferred = dotnetToolX86
32+
}
33+
34+
// Search order:
35+
// 1) sibling to yc executable
36+
// 2) PATH
37+
// 3) fallback default name (yc-dot-net.exe)
38+
if p, ok := findToolNearYcOrPath(preferred); ok {
39+
return p, true, nil
40+
}
41+
if p, ok := findToolNearYcOrPath(config.DefaultDotnetToolName); ok {
42+
return p, true, nil
43+
}
44+
45+
return "", false, fmt.Errorf(".NET helper for target PID %d (%s) not found. expected %s (or %s)", pid, targetArch, preferred, config.DefaultDotnetToolName)
46+
}
47+
48+
func findToolNearYcOrPath(toolName string) (string, bool) {
49+
if exePath, err := os.Executable(); err == nil {
50+
candidate := filepath.Join(filepath.Dir(exePath), toolName)
51+
if _, statErr := os.Stat(candidate); statErr == nil {
52+
return candidate, true
53+
}
54+
}
55+
if resolved, err := exec.LookPath(toolName); err == nil {
56+
return resolved, true
57+
}
58+
return "", false
59+
}
60+
61+
func detectWindowsProcessArch(pid int) (string, error) {
62+
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
63+
if err != nil {
64+
return "", err
65+
}
66+
defer windows.CloseHandle(h)
67+
68+
var targetWow64 bool
69+
if err = windows.IsWow64Process(h, &targetWow64); err != nil {
70+
return "", err
71+
}
72+
73+
// On 32-bit Windows, everything is x86.
74+
if isOS64Bit() {
75+
if targetWow64 {
76+
return "x86", nil
77+
}
78+
return "x64", nil
79+
}
80+
return "x86", nil
81+
}
82+
83+
func isOS64Bit() bool {
84+
return os.Getenv("PROCESSOR_ARCHITEW6432") != "" || os.Getenv("PROCESSOR_ARCHITECTURE") == "AMD64"
85+
}

internal/capture/dotnet_utils.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strconv"
78
"strings"
89
"yc-agent/internal/capture/executils"
910
"yc-agent/internal/config"
@@ -53,10 +54,86 @@ func ensureDotnetToolResolved() (string, error) {
5354
return resolved, nil
5455
}
5556

57+
func tryParsePid(value string) (int, bool) {
58+
if value == "" {
59+
return 0, false
60+
}
61+
pid, err := strconv.Atoi(value)
62+
if err != nil || pid <= 0 {
63+
return 0, false
64+
}
65+
return pid, true
66+
}
67+
68+
// extractTargetPidFromArgs supports flexible argument ordering, including:
69+
// - "-p 1234" / "--pid 1234"
70+
// - "-p=1234" / "--pid=1234"
71+
// - positional numeric argument (legacy/fallback)
72+
func extractTargetPidFromArgs(args []string) (int, bool) {
73+
// Prefer explicit pid flags first.
74+
for i := 0; i < len(args); i++ {
75+
arg := strings.TrimSpace(args[i])
76+
switch {
77+
case arg == "-p" || arg == "--pid":
78+
if i+1 < len(args) {
79+
if pid, ok := tryParsePid(strings.TrimSpace(args[i+1])); ok {
80+
return pid, true
81+
}
82+
}
83+
case strings.HasPrefix(arg, "-p="):
84+
if pid, ok := tryParsePid(strings.TrimPrefix(arg, "-p=")); ok {
85+
return pid, true
86+
}
87+
case strings.HasPrefix(arg, "--pid="):
88+
if pid, ok := tryParsePid(strings.TrimPrefix(arg, "--pid=")); ok {
89+
return pid, true
90+
}
91+
}
92+
}
93+
94+
// Fallback: first positive integer token in args.
95+
for _, arg := range args {
96+
if pid, ok := tryParsePid(strings.TrimSpace(arg)); ok {
97+
return pid, true
98+
}
99+
}
100+
return 0, false
101+
}
102+
103+
// resolveDotnetToolForArgs picks the best .NET helper binary for the command:
104+
// - If user configured DotnetToolPath explicitly, keep using it.
105+
// - Else, if a target PID can be extracted from args, prefer architecture-specific
106+
// helper (yc-dot-net-x86.exe / yc-dot-net-x64.exe) and fall back to default resolver.
107+
func resolveDotnetToolForArgs(args []string) (string, error) {
108+
if path := config.GlobalConfig.DotnetToolPath; path != "" {
109+
return path, nil
110+
}
111+
112+
// Reuse the canonical pid parsed by config flags first (-p can appear anywhere).
113+
if pid, ok := tryParsePid(strings.TrimSpace(config.GlobalConfig.Pid)); ok {
114+
if tool, found, err := resolveDotnetToolByPid(pid); err != nil {
115+
return "", err
116+
} else if found {
117+
return tool, nil
118+
}
119+
}
120+
121+
// Fallback for direct/internal invocations that bypass GlobalConfig parsing.
122+
if pid, ok := extractTargetPidFromArgs(args); ok {
123+
if tool, found, err := resolveDotnetToolByPid(pid); err != nil {
124+
return "", err
125+
} else if found {
126+
return tool, nil
127+
}
128+
}
129+
130+
return ensureDotnetToolResolved()
131+
}
132+
56133
// executeDotnetTool runs the configured .NET helper executable with the given arguments
57134
// and captures the output to a file. Returns the file handle and any error.
58135
func executeDotnetTool(args []string, outputPath string) (*os.File, error) {
59-
toolPath, err := ensureDotnetToolResolved()
136+
toolPath, err := resolveDotnetToolForArgs(args)
60137
if err != nil {
61138
return nil, err
62139
}
@@ -147,7 +224,7 @@ func executeDotnetTool(args []string, outputPath string) (*os.File, error) {
147224
// startDotnetToolInBackground starts the configured .NET helper executable with the
148225
// given arguments and returns the running command handle without waiting.
149226
func startDotnetToolInBackground(args []string, hookers ...executils.Hooker) (executils.CmdManager, error) {
150-
toolPath, err := ensureDotnetToolResolved()
227+
toolPath, err := resolveDotnetToolForArgs(args)
151228
if err != nil {
152229
return nil, err
153230
}

0 commit comments

Comments
 (0)