ctx/internal/plugins/csharp/helper/process.go
Ricardo Carneiro 22f6e8fde9 chore: add golangci-lint, GitHub Actions CI, and pre-commit hooks
Adds .golangci.yml (errcheck, govet, staticcheck, revive, gocyclo,
gocritic, nilerr, errorlint, etc.). Fixes all lint issues found:
- RpcError/wrapRpc renamed to RPCError/wrapRPC (revive var-naming)
- Shadow vars resolved in process.go, outline.go, project.go
- nilerr suppressed with justifying comments in git/collect.go and
  auto/detector.go (WalkDir / intentional empty-returns)
- if-else chain rewritten as switch in format.go (gocritic)
- gofmt applied to detector.go and client.go

Adds .github/workflows/ci.yml: lint (ubuntu), build+vet+test matrix
(ubuntu/windows/macos), and Roslyn helper build (windows). Validated
with actionlint (0 issues).

Adds .githooks/pre-commit + pre-commit.ps1 for local pre-commit checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 00:30:54 -03:00

109 lines
2.6 KiB
Go

package helper
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os/exec"
"sync/atomic"
)
// Process manages the lifetime of the roslyn-helper subprocess.
// Calls are sequential — no concurrency within a single Process.
type Process struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout *bufio.Reader
nextID atomic.Int64
}
// Start launches the helper subprocess and returns a Process ready to use.
func Start(helperPath string) (*Process, error) {
cmd := exec.Command(helperPath)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("helper stdin pipe: %w", err)
}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("helper stdout pipe: %w", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("helper stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("helper start: %w", err)
}
// Drain stderr in background to prevent blocking.
go func() { _, _ = io.Copy(io.Discard, stderrPipe) }()
p := &Process{
cmd: cmd,
stdin: stdin,
stdout: bufio.NewReader(stdoutPipe),
}
return p, nil
}
// Send sends a JSON-RPC request and returns the raw result JSON.
// Returns an error if the helper returns an RpcError or dies.
func (p *Process) Send(method string, params interface{}) (json.RawMessage, error) {
id := int(p.nextID.Add(1))
var rawParams json.RawMessage
if params != nil {
var err error
rawParams, err = json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("marshal params: %w", err)
}
}
req := Request{ID: id, Method: method, Params: rawParams}
line, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
line = append(line, '\n')
if _, werr := p.stdin.Write(line); werr != nil {
return nil, fmt.Errorf("helper write (process may have crashed): %w", werr)
}
respLine, err := p.stdout.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("helper read (process may have crashed): %w", err)
}
var resp Response
if err := json.Unmarshal([]byte(respLine), &resp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if resp.ID != id {
return nil, fmt.Errorf("response id mismatch: got %d, want %d", resp.ID, id)
}
if resp.Error != nil {
return nil, resp.Error
}
return resp.Result, nil
}
// Close sends a shutdown request, closes stdin, and waits for the process to exit.
func (p *Process) Close() error {
// Best-effort shutdown — ignore errors here.
_, _ = p.Send("shutdown", nil)
_ = p.stdin.Close()
_ = p.cmd.Wait()
return nil
}