Skip to content

Commit c079e89

Browse files
authored
Merge pull request #35 from ycrash/hotfix/platform-mismatch
hotfix/platform mismatch
2 parents e62b2e5 + 0698615 commit c079e89

12 files changed

Lines changed: 187 additions & 67 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ require (
7373
github.com/yusufpapurcu/wmi v1.2.4 // indirect
7474
golang.org/x/net v0.45.0 // indirect
7575
golang.org/x/oauth2 v0.27.0 // indirect
76-
golang.org/x/sys v0.36.0 // indirect
76+
golang.org/x/sys v0.36.0
7777
golang.org/x/term v0.35.0 // indirect
7878
golang.org/x/text v0.29.0 // indirect
7979
golang.org/x/time v0.7.0 // indirect

internal/agent/m3/m3.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,6 @@ func (m3 *M3App) captureAndTransmit(pids map[int]string, endpoint string) {
250250
}
251251
}
252252

253-
// Eagerly resolve the .NET helper path once per cycle when any .NET
254-
// targets exist, so failures surface early rather than per-capture.
255-
if len(dotnetPIDs) > 0 {
256-
if resolved, err := config.ResolveDotnetToolPath(); err != nil {
257-
logger.Warn().Err(err).Msg(".NET helper not found - .NET captures will fail")
258-
} else if config.GlobalConfig.DotnetToolPath == "" {
259-
config.GlobalConfig.DotnetToolPath = resolved
260-
}
261-
}
262-
263253
if m3.AsyncDotNetGCCapture != nil {
264254
// Reconcile creates/updates async GC capture sessions for current .NET PIDs.
265255
// In the per-PID loop below, uploadDotnetGCM3 reads and uploads artifacts from

internal/agent/ondemand/ondemand.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,6 @@ Ignored errors: %v
289289
appRuntime := config.GetAppRuntime(pid)
290290

291291
if appRuntime == "dotnet" {
292-
// Eagerly resolve the .NET helper path so failures surface early,
293-
// before any capture goroutines are launched.
294-
if resolved, err := config.ResolveDotnetToolPath(); err != nil {
295-
logger.Warn().Err(err).Msg(".NET helper not found - .NET captures will fail")
296-
} else if config.GlobalConfig.DotnetToolPath == "" {
297-
config.GlobalConfig.DotnetToolPath = resolved
298-
}
299292
// ------------------------------------------------------------------------------
300293
// .NET runtime captures
301294
// ------------------------------------------------------------------------------

internal/capture/dotnet_gc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (d *DotnetGC) CaptureToFile() (*os.File, error) {
9696
}
9797

9898
// Execute the dotnet tool and capture output
99-
file, err := executeDotnetTool(args, fmt.Sprintf(dotnetGCOutputPath, d.Pid))
99+
file, err := executeDotnetTool(d.Pid, args, fmt.Sprintf(dotnetGCOutputPath, d.Pid))
100100
if err != nil {
101101
return nil, fmt.Errorf("failed to capture .NET GC events: %w", err)
102102
}

internal/capture/dotnet_gc_async.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (d *DotnetGCAsync) ensureStartedLocked(pid int, appName string) error {
121121
}
122122

123123
args := []string{"-gc", strconv.Itoa(pid), d.baseDir, "-1"}
124-
cmd, err := startDotnetToolInBackground(args, executils.DirHooker{Dir: d.baseDir})
124+
cmd, err := startDotnetToolInBackground(pid, args, executils.DirHooker{Dir: d.baseDir})
125125
if err != nil {
126126
return err
127127
}

internal/capture/dotnet_heap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (d *DotnetHeap) CaptureToFile() (*os.File, error) {
4949
}
5050

5151
// Execute the dotnet tool and capture output
52-
file, err := executeDotnetTool(args, fmt.Sprintf(dotnetHeapOutputPath, d.Pid))
52+
file, err := executeDotnetTool(d.Pid, args, fmt.Sprintf(dotnetHeapOutputPath, d.Pid))
5353
if err != nil {
5454
return nil, fmt.Errorf("failed to capture .NET heap statistics: %w", err)
5555
}

internal/capture/dotnet_thread.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (d *DotnetThread) CaptureToFile() (*os.File, error) {
4949
}
5050

5151
// Execute the dotnet tool and capture output
52-
file, err := executeDotnetTool(args, fmt.Sprintf(dotnetThreadOutputPath, d.Pid))
52+
file, err := executeDotnetTool(d.Pid, args, fmt.Sprintf(dotnetThreadOutputPath, d.Pid))
5353
if err != nil {
5454
return nil, fmt.Errorf("failed to capture .NET thread dump: %w", err)
5555
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//go:build !windows
2+
3+
package capture
4+
5+
func detectTargetArch(pid int) (string, error) {
6+
// pid is unused on non-Windows;
7+
// kept to match the Windows signature, discarded here to silence unusedparams lint
8+
_ = pid
9+
return "", nil
10+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//go:build windows
2+
3+
package capture
4+
5+
import (
6+
"os"
7+
8+
"golang.org/x/sys/windows"
9+
)
10+
11+
func detectTargetArch(pid int) (string, error) {
12+
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
13+
if err != nil {
14+
return "", err
15+
}
16+
defer windows.CloseHandle(h)
17+
18+
var targetWow64 bool
19+
if err = windows.IsWow64Process(h, &targetWow64); err != nil {
20+
return "", err
21+
}
22+
23+
// On 32-bit Windows, everything is x86.
24+
if isOS64Bit() {
25+
if targetWow64 {
26+
return "x86", nil
27+
}
28+
return "x64", nil
29+
}
30+
return "x86", nil
31+
}
32+
33+
func isOS64Bit() bool {
34+
return os.Getenv("PROCESSOR_ARCHITEW6432") != "" || os.Getenv("PROCESSOR_ARCHITECTURE") == "AMD64"
35+
}

internal/capture/dotnet_utils.go

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ var knownDotnetToolErrors = []dotnetToolFriendlyError{
2626
},
2727
}
2828

29+
const DotnetSourceUserOverride = "user-override"
30+
const DotnetSourceArchMatched = "arch-matched"
31+
const DotnetSourceDefault = "default"
32+
33+
type DotnetToolResolution struct {
34+
Path string
35+
Source string // "user-override", "arch-matched", "default"
36+
}
37+
2938
// wrapDotnetToolStartError wraps a command-start error, appending a
3039
// user-friendly message when the error matches a known pattern. The original
3140
// error message is always preserved for debugging.
@@ -39,30 +48,61 @@ func wrapDotnetToolStartError(err error, cmdArgs []string) error {
3948
return fmt.Errorf("failed to start dotnet tool %v: %w", cmdArgs, err)
4049
}
4150

42-
// ensureDotnetToolResolved lazily resolves DotnetToolPath if it was not set
43-
// during validation (e.g. when runtime was auto-detected rather than explicit).
44-
func ensureDotnetToolResolved() (string, error) {
45-
if path := config.GlobalConfig.DotnetToolPath; path != "" {
46-
return path, nil
51+
func resolveDotnetToolForPid(pid int) (DotnetToolResolution, error) {
52+
// user override
53+
if config.GlobalConfig.DotnetToolPath != "" {
54+
resolvedPath, err := config.ResolveDotnetToolOverride()
55+
if err != nil {
56+
return DotnetToolResolution{}, err
57+
}
58+
return DotnetToolResolution{Path: resolvedPath, Source: DotnetSourceUserOverride}, nil
4759
}
48-
resolved, err := config.ResolveDotnetToolPath()
49-
if err != nil {
50-
return "", err
60+
61+
// arch matched
62+
arch, detectErr := detectTargetArch(pid)
63+
if detectErr != nil {
64+
logger.Warn().Err(detectErr).Int("pid", pid).Msg("could not detect target arch")
65+
}
66+
if arch != "" {
67+
name := config.DotnetToolNameForArch(arch)
68+
if p, ok := config.FindDotnetToolNearYcOrPath(name); ok {
69+
return DotnetToolResolution{Path: p, Source: DotnetSourceArchMatched}, nil
70+
}
71+
72+
return DotnetToolResolution{}, fmt.Errorf(".NET tool for PID %d (%s) not found. expected %s next to yc or on PATH", pid, arch, name)
73+
}
74+
75+
// No arch info — fall back to default tool name
76+
if p, ok := config.FindDotnetToolNearYcOrPath(config.DefaultDotnetToolName); ok {
77+
if detectErr != nil {
78+
logger.Warn().
79+
Err(detectErr).
80+
Int("pid", pid).
81+
Str("path", p).
82+
Msg("using legacy .NET tool path because target arch detection failed")
83+
}
84+
return DotnetToolResolution{Path: p, Source: DotnetSourceDefault}, nil
85+
}
86+
87+
if detectErr != nil {
88+
return DotnetToolResolution{}, fmt.Errorf(
89+
".NET tool path %q not found near yc or on PATH (target arch detection for PID %d failed: %w)",
90+
config.DefaultDotnetToolName, pid, detectErr)
5191
}
52-
config.GlobalConfig.DotnetToolPath = resolved
53-
return resolved, nil
92+
return DotnetToolResolution{}, fmt.Errorf(
93+
".NET tool path %q not found near yc or on PATH", config.DefaultDotnetToolName)
5494
}
5595

56-
// executeDotnetTool runs the configured .NET helper executable with the given arguments
96+
// executeDotnetTool runs the configured .NET tool executable with the given arguments
5797
// and captures the output to a file. Returns the file handle and any error.
58-
func executeDotnetTool(args []string, outputPath string) (*os.File, error) {
59-
toolPath, err := ensureDotnetToolResolved()
98+
func executeDotnetTool(pid int, args []string, outputPath string) (*os.File, error) {
99+
toolResolution, err := resolveDotnetToolForPid(pid)
60100
if err != nil {
61101
return nil, err
62102
}
63103

64104
// Build the command: [toolPath, args...]
65-
cmdArgs := append([]string{toolPath}, args...)
105+
cmdArgs := append([]string{toolResolution.Path}, args...)
66106

67107
logger.Log("Executing dotnet tool: %v", cmdArgs)
68108

@@ -144,15 +184,15 @@ func executeDotnetTool(args []string, outputPath string) (*os.File, error) {
144184
return file, nil
145185
}
146186

147-
// startDotnetToolInBackground starts the configured .NET helper executable with the
187+
// startDotnetToolInBackground starts the configured .NET tool executable with the
148188
// given arguments and returns the running command handle without waiting.
149-
func startDotnetToolInBackground(args []string, hookers ...executils.Hooker) (executils.CmdManager, error) {
150-
toolPath, err := ensureDotnetToolResolved()
189+
func startDotnetToolInBackground(pid int, args []string, hookers ...executils.Hooker) (executils.CmdManager, error) {
190+
toolResolution, err := resolveDotnetToolForPid(pid)
151191
if err != nil {
152192
return nil, err
153193
}
154194

155-
cmdArgs := append([]string{toolPath}, args...)
195+
cmdArgs := append([]string{toolResolution.Path}, args...)
156196
logger.Log("Starting dotnet tool in background: %v", cmdArgs)
157197

158198
cmd, err := executils.CommandStartInBackground(cmdArgs, hookers...)

0 commit comments

Comments
 (0)