diff --git a/pkg/desktop/transport/transport_test.go b/pkg/desktop/transport/transport_test.go index b7f06211fd..7080001c17 100644 --- a/pkg/desktop/transport/transport_test.go +++ b/pkg/desktop/transport/transport_test.go @@ -243,7 +243,7 @@ func TestFallbackTransport_NonSocketErrorDoesNotDisableProxy(t *testing.T) { req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.invalid/", http.NoBody) require.NoError(t, err) - resp, err := ft.RoundTrip(req) //nolint:bodyclose // resp is nil on error, checked below + resp, err := ft.RoundTrip(req) //nolint:bodyclose,nolintlint // resp is nil on error, checked below require.Error(t, err) require.Nil(t, resp) assert.True(t, errors.Is(err, upstreamErr) || err.Error() == upstreamErr.Error()) diff --git a/pkg/selfupdate/exec_windows.go b/pkg/selfupdate/exec_windows.go index 6517efcc91..64217cd50b 100644 --- a/pkg/selfupdate/exec_windows.go +++ b/pkg/selfupdate/exec_windows.go @@ -3,6 +3,7 @@ package selfupdate import ( + "context" "fmt" "os" "os/exec" @@ -27,9 +28,9 @@ func swapBinary(dst, src string) error { if cpErr := atomicWriteFromFile(dst, src); cpErr != nil { // Roll back so we never leave the install without a binary. if rbErr := os.Rename(old, dst); rbErr != nil { - return fmt.Errorf("installing new binary: %w (copy fallback failed: %v; rollback also failed: %v)", err, cpErr, rbErr) + return fmt.Errorf("installing new binary: %w (copy fallback failed: %w; rollback also failed: %w)", err, cpErr, rbErr) } - return fmt.Errorf("installing new binary: %w (copy fallback failed: %v)", err, cpErr) + return fmt.Errorf("installing new binary: %w (copy fallback failed: %w)", err, cpErr) } _ = os.Remove(src) } @@ -48,7 +49,7 @@ func reExecProcess(path string, args, env []string) error { childArgs = args[1:] } - cmd := exec.Command(path, childArgs...) //nolint:gosec // path is our own freshly installed binary + cmd := exec.CommandContext(context.Background(), path, childArgs...) cmd.Env = env cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 4c006f2ac9..f3c54afb01 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -453,7 +453,7 @@ func (r *Repo) DiffFull(ctx context.Context, from, to string) ([]FileDiff, error } func (r *Repo) ensure(ctx context.Context) error { - if err := os.MkdirAll(r.gitdir, 0o755); err != nil { //nolint:gosec // 0o755 matches the layout `git init` itself creates + if err := os.MkdirAll(r.gitdir, 0o755); err != nil { //nolint:gosec,nolintlint // 0o755 matches the layout `git init` itself creates return err } if _, err := os.Stat(filepath.Join(r.gitdir, "HEAD")); err == nil { @@ -623,10 +623,10 @@ func (r *Repo) syncExcludes(ctx context.Context, list []string) error { text += "\n" } target := filepath.Join(r.gitdir, "info", "exclude") - if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { //nolint:gosec // mirrors what git writes into .git/info + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { //nolint:gosec,nolintlint // mirrors what git writes into .git/info return err } - return os.WriteFile(target, []byte(text), 0o644) //nolint:gosec // mirrors what git writes for .git/info/exclude + return os.WriteFile(target, []byte(text), 0o644) //nolint:gosec,nolintlint // mirrors what git writes for .git/info/exclude } func (r *Repo) args(cmd ...string) []string { diff --git a/pkg/tools/builtin/backgroundjobs/backgroundjobs.go b/pkg/tools/builtin/backgroundjobs/backgroundjobs.go index 20f8cc0df0..e8f5adb0a7 100644 --- a/pkg/tools/builtin/backgroundjobs/backgroundjobs.go +++ b/pkg/tools/builtin/backgroundjobs/backgroundjobs.go @@ -37,6 +37,7 @@ const ( ToolNameWaitBackgroundJob = "wait_background_job" maxBackgroundJobOutputBytes = 10 * 1024 * 1024 + waitDelayAfterJobExit = 1 * time.Second ) // ToolSet manages long-running shell commands. @@ -224,6 +225,7 @@ func (h *backgroundJobsHandler) runBackgroundJob(ctx context.Context, rt tools.R cmd.Env = h.env cmd.Dir = h.resolveWorkDir(params.Cwd) cmd.SysProcAttr = platformSpecificSysProcAttr() + cmd.WaitDelay = waitDelayAfterJobExit job := &backgroundJob{ id: jobID, @@ -278,17 +280,23 @@ func (h *backgroundJobsHandler) monitorJob(ctx context.Context, job *backgroundJ return } + var newStatus int32 if err != nil { if exitErr, ok := errors.AsType[*exec.ExitError](err); ok { job.exitCode = exitErr.ExitCode() } else { job.exitCode = -1 } - job.status.Store(statusFailed) + newStatus = statusFailed job.err = err } else { job.exitCode = 0 - job.status.Store(statusCompleted) + newStatus = statusCompleted + } + + if !job.status.CompareAndSwap(statusRunning, newStatus) { + job.outputMu.Unlock() + return } status := job.status.Load() diff --git a/pkg/tools/builtin/backgroundjobs/backgroundjobs_test.go b/pkg/tools/builtin/backgroundjobs/backgroundjobs_test.go index 50e4936e1d..72f6257125 100644 --- a/pkg/tools/builtin/backgroundjobs/backgroundjobs_test.go +++ b/pkg/tools/builtin/backgroundjobs/backgroundjobs_test.go @@ -3,6 +3,7 @@ package backgroundjobs import ( "context" "encoding/json" + "path/filepath" "runtime" "testing" "time" @@ -398,7 +399,12 @@ func TestBackgroundJobsTool_Instructions(t *testing.T) { func TestResolveWorkDir(t *testing.T) { t.Parallel() - workingDir := "/configured/project" + workingDir := filepath.FromSlash("/configured/project") + absOther := filepath.FromSlash("/tmp/another") + if runtime.GOOS == "windows" { + workingDir = `C:\configured\project` + absOther = `C:\tmp\another` + } h := &backgroundJobsHandler{workingDir: workingDir} tests := []struct { @@ -408,10 +414,10 @@ func TestResolveWorkDir(t *testing.T) { }{ {name: "empty defaults to workingDir", cwd: "", expected: workingDir}, {name: "dot defaults to workingDir", cwd: ".", expected: workingDir}, - {name: "absolute path unchanged", cwd: "/tmp/other", expected: "/tmp/other"}, - {name: "relative path joined with workingDir", cwd: "src/pkg", expected: "/configured/project/src/pkg"}, - {name: "relative with dot prefix", cwd: "./subdir", expected: "/configured/project/subdir"}, - {name: "relative with parent traversal", cwd: "../sibling", expected: "/configured/sibling"}, + {name: "absolute path unchanged", cwd: absOther, expected: absOther}, + {name: "relative path joined with workingDir", cwd: "src/pkg", expected: filepath.Join(workingDir, "src", "pkg")}, + {name: "relative with dot prefix", cwd: "./subdir", expected: filepath.Join(workingDir, "subdir")}, + {name: "relative with parent traversal", cwd: "../sibling", expected: filepath.Clean(filepath.Join(workingDir, "..", "sibling"))}, } for _, tt := range tests { diff --git a/pkg/tools/builtin/backgroundjobs/cmd_windows.go b/pkg/tools/builtin/backgroundjobs/cmd_windows.go index d25a83ef15..ac0aa0e0f2 100644 --- a/pkg/tools/builtin/backgroundjobs/cmd_windows.go +++ b/pkg/tools/builtin/backgroundjobs/cmd_windows.go @@ -31,13 +31,13 @@ func createProcessGroup(proc *os.Process) (*processGroup, error) { if _, err := windows.SetInformationJobObject( job, windows.JobObjectExtendedLimitInformation, - uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Pointer(&info)), //nolint:gosec // Windows syscall requires unsafe pointer uint32(unsafe.Sizeof(info))); err != nil { _ = windows.CloseHandle(job) return nil, err } - handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) + handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) //nolint:gosec // Windows PID fits in uint32 if err != nil { _ = windows.CloseHandle(job) return nil, err diff --git a/pkg/tools/builtin/shell/cmd_windows.go b/pkg/tools/builtin/shell/cmd_windows.go index 05e11368ac..02d1c9fa71 100644 --- a/pkg/tools/builtin/shell/cmd_windows.go +++ b/pkg/tools/builtin/shell/cmd_windows.go @@ -31,13 +31,13 @@ func createProcessGroup(proc *os.Process) (*processGroup, error) { if _, err := windows.SetInformationJobObject( job, windows.JobObjectExtendedLimitInformation, - uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Pointer(&info)), //nolint:gosec // Windows syscall requires unsafe pointer uint32(unsafe.Sizeof(info))); err != nil { _ = windows.CloseHandle(job) return nil, err } - handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) + handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) //nolint:gosec // Windows PID fits in uint32 if err != nil { _ = windows.CloseHandle(job) return nil, err