diff --git a/internal/app/cmd/daemon.go b/internal/app/cmd/daemon.go index 3f4812f..eda2e40 100644 --- a/internal/app/cmd/daemon.go +++ b/internal/app/cmd/daemon.go @@ -163,7 +163,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu bandwidthManager.Start(ctx) log.Infof("bandwidth manager started, testing against: %s", reg.Address) - capabilities := envcheck.DetectCapabilities(ctx, dockerHost) + capabilities := envcheck.DetectCapabilities(ctx, dockerHost, cfg.Container.WorkdirParent) // Include initial bandwidth result if available capabilities.Bandwidth = bandwidthManager.GetLastResult() capabilitiesJson := capabilities.ToJSON() @@ -186,7 +186,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu } // Start periodic capabilities update goroutine - go periodicCapabilitiesUpdate(ctx, runner, ls.Names(), dockerHost) + go periodicCapabilitiesUpdate(ctx, runner, ls.Names(), dockerHost, cfg.Container.WorkdirParent) poller := poll.New(cfg, cli, runner) poller.SetBandwidthManager(bandwidthManager) @@ -240,7 +240,7 @@ func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) { } // periodicCapabilitiesUpdate periodically updates capabilities including disk space and bandwidth -func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string) { +func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string, workingDir string) { ticker := time.NewTicker(CapabilitiesUpdateInterval) defer ticker.Stop() @@ -254,7 +254,7 @@ func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNa return case <-ticker.C: // Detect updated capabilities (disk space changes over time) - capabilities := envcheck.DetectCapabilities(ctx, dockerHost) + capabilities := envcheck.DetectCapabilities(ctx, dockerHost, workingDir) // Include latest bandwidth result if bandwidthManager != nil { diff --git a/internal/app/poll/poller.go b/internal/app/poll/poller.go index 734e1da..1b2a311 100644 --- a/internal/app/poll/poller.go +++ b/internal/app/poll/poller.go @@ -165,7 +165,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) { defer cancel() // Detect capabilities including current disk space - caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost) + caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost, p.cfg.Container.WorkdirParent) // Include latest bandwidth result if available if p.bandwidthManager != nil { diff --git a/internal/pkg/envcheck/capabilities.go b/internal/pkg/envcheck/capabilities.go index 30f2117..fe38300 100644 --- a/internal/pkg/envcheck/capabilities.go +++ b/internal/pkg/envcheck/capabilities.go @@ -19,6 +19,7 @@ import ( // DiskInfo holds disk space information type DiskInfo struct { + Path string `json:"path,omitempty"` // Path being checked (working directory) Total uint64 `json:"total_bytes"` Free uint64 `json:"free_bytes"` Used uint64 `json:"used_bytes"` @@ -69,7 +70,8 @@ type CapabilityFeatures struct { } // DetectCapabilities detects the runner's capabilities -func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilities { +// workingDir is the directory where builds will run (for disk space detection) +func DetectCapabilities(ctx context.Context, dockerHost string, workingDir string) *RunnerCapabilities { cap := &RunnerCapabilities{ OS: runtime.GOOS, Arch: runtime.GOARCH, @@ -115,8 +117,8 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit // Detect package managers detectPackageManagers(ctx, cap) - // Detect disk space - cap.Disk = detectDiskSpace() + // Detect disk space on the working directory's filesystem + cap.Disk = detectDiskSpace(workingDir) // Generate suggested labels based on detected capabilities cap.SuggestedLabels = generateSuggestedLabels(cap) @@ -158,7 +160,8 @@ func detectXcode(ctx context.Context) *XcodeInfo { continue // Skip header lines } if strings.Contains(line, "iOS") || strings.Contains(line, "macOS") || - strings.Contains(line, "watchOS") || strings.Contains(line, "tvOS") { + strings.Contains(line, "watchOS") || strings.Contains(line, "tvOS") || + strings.Contains(line, "visionOS") || strings.Contains(line, "xrOS") { // Extract SDK name if idx := strings.Index(line, "-sdk"); idx != -1 { sdkPart := strings.TrimSpace(line[:idx]) @@ -267,11 +270,20 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string { // Xcode/iOS labels (macOS only) if cap.Xcode != nil { addLabel("xcode") - // Check for iOS SDK + // Check for SDKs for _, sdk := range cap.Xcode.SDKs { - if strings.Contains(strings.ToLower(sdk), "ios") { + sdkLower := strings.ToLower(sdk) + if strings.Contains(sdkLower, "ios") { addLabel("ios") - break + } + if strings.Contains(sdkLower, "visionos") || strings.Contains(sdkLower, "xros") { + addLabel("visionos") + } + if strings.Contains(sdkLower, "watchos") { + addLabel("watchos") + } + if strings.Contains(sdkLower, "tvos") { + addLabel("tvos") } } // If simulators available, add simulator label @@ -395,18 +407,19 @@ func detectDockerCompose(ctx context.Context) bool { func detectTools(ctx context.Context, cap *RunnerCapabilities) { toolDetectors := map[string]func(context.Context) []string{ - "node": detectNodeVersions, - "go": detectGoVersions, - "python": detectPythonVersions, - "java": detectJavaVersions, - "dotnet": detectDotnetVersions, - "rust": detectRustVersions, - "ruby": detectRubyVersions, - "php": detectPHPVersions, - "swift": detectSwiftVersions, - "kotlin": detectKotlinVersions, - "flutter": detectFlutterVersions, - "dart": detectDartVersions, + "node": detectNodeVersions, + "go": detectGoVersions, + "python": detectPythonVersions, + "java": detectJavaVersions, + "dotnet": detectDotnetVersions, + "rust": detectRustVersions, + "ruby": detectRubyVersions, + "php": detectPHPVersions, + "swift": detectSwiftVersions, + "kotlin": detectKotlinVersions, + "flutter": detectFlutterVersions, + "dart": detectDartVersions, + "powershell": detectPowerShellVersions, } for tool, detector := range toolDetectors { @@ -763,6 +776,50 @@ func detectDartVersions(ctx context.Context) []string { return detectToolVersion(ctx, "dart", "--version", "Dart SDK version: ") } +func detectPowerShellVersions(ctx context.Context) []string { + versions := []string{} + + // Check for pwsh (PowerShell Core / PowerShell 7+) + if v := detectPwshVersion(ctx, "pwsh"); v != "" { + versions = append(versions, "pwsh:"+v) + } + + // Check for powershell (Windows PowerShell 5.x) + if runtime.GOOS == "windows" { + if v := detectPwshVersion(ctx, "powershell"); v != "" { + versions = append(versions, "powershell:"+v) + } + } + + return versions +} + +func detectPwshVersion(ctx context.Context, cmd string) string { + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + // Use -Command to get version + var c *exec.Cmd + if cmd == "pwsh" { + c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()") + } else { + c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()") + } + + output, err := c.Output() + if err != nil { + return "" + } + + version := strings.TrimSpace(string(output)) + // Return major.minor + parts := strings.Split(version, ".") + if len(parts) >= 2 { + return parts[0] + "." + parts[1] + } + return version +} + func detectSimpleToolVersion(ctx context.Context, cmd string) string { if _, err := exec.LookPath(cmd); err != nil { return "" diff --git a/internal/pkg/envcheck/disk_unix.go b/internal/pkg/envcheck/disk_unix.go index cc10e16..301bd6c 100644 --- a/internal/pkg/envcheck/disk_unix.go +++ b/internal/pkg/envcheck/disk_unix.go @@ -9,13 +9,23 @@ import ( "golang.org/x/sys/unix" ) -// detectDiskSpace detects disk space on the root filesystem (Unix version) -func detectDiskSpace() *DiskInfo { +// detectDiskSpace detects disk space on the specified path's filesystem (Unix version) +// If path is empty, defaults to "/" +func detectDiskSpace(path string) *DiskInfo { + if path == "" { + path = "/" + } + var stat unix.Statfs_t - err := unix.Statfs("/", &stat) + err := unix.Statfs(path, &stat) if err != nil { - return nil + // Fallback to root if the path doesn't exist + err = unix.Statfs("/", &stat) + if err != nil { + return nil + } + path = "/" } total := stat.Blocks * uint64(stat.Bsize) @@ -24,6 +34,7 @@ func detectDiskSpace() *DiskInfo { usedPercent := float64(used) / float64(total) * 100 return &DiskInfo{ + Path: path, Total: total, Free: free, Used: used, diff --git a/internal/pkg/envcheck/disk_windows.go b/internal/pkg/envcheck/disk_windows.go index 06ac06a..065134e 100644 --- a/internal/pkg/envcheck/disk_windows.go +++ b/internal/pkg/envcheck/disk_windows.go @@ -6,23 +6,49 @@ package envcheck import ( + "path/filepath" + "golang.org/x/sys/windows" ) -// detectDiskSpace detects disk space on the C: drive (Windows version) -func detectDiskSpace() *DiskInfo { +// detectDiskSpace detects disk space on the specified path's drive (Windows version) +// If path is empty, defaults to "C:\" +func detectDiskSpace(path string) *DiskInfo { + if path == "" { + path = "C:\\" + } + + // Resolve to absolute path + absPath, err := filepath.Abs(path) + if err != nil { + absPath = "C:\\" + } + + // Extract drive letter (e.g., "D:\" from "D:\builds\runner") + drivePath := filepath.VolumeName(absPath) + "\\" + if drivePath == "\\" { + drivePath = "C:\\" + } + var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64 - path := windows.StringToUTF16Ptr("C:\\") - err := windows.GetDiskFreeSpaceEx(path, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes) + pathPtr := windows.StringToUTF16Ptr(drivePath) + err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes) if err != nil { - return nil + // Fallback to C: drive + pathPtr = windows.StringToUTF16Ptr("C:\\") + err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes) + if err != nil { + return nil + } + drivePath = "C:\\" } used := totalNumberOfBytes - totalNumberOfFreeBytes usedPercent := float64(used) / float64(totalNumberOfBytes) * 100 return &DiskInfo{ + Path: drivePath, Total: totalNumberOfBytes, Free: totalNumberOfFreeBytes, Used: used,