style: fix whitespace/gofumpt/golines in linux/unix build-tagged files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-05 02:16:07 +09:00
parent 0db2afef68
commit 12c61899f5
4 changed files with 63 additions and 477 deletions

View file

@ -8,465 +8,285 @@ import (
) )
// I2C ioctl constants from Linux kernel headers (<linux/i2c-dev.h>, <linux/i2c.h>) // I2C ioctl constants from Linux kernel headers (<linux/i2c-dev.h>, <linux/i2c.h>)
const ( const (
i2cSlave = 0x0703 // Set slave address (fails if in use by driver) i2cSlave = 0x0703 // Set slave address (fails if in use by driver)
i2cFuncs = 0x0705 // Query adapter functionality bitmask i2cFuncs = 0x0705 // Query adapter functionality bitmask
i2cSmbus = 0x0720 // Perform SMBus transaction i2cSmbus = 0x0720 // Perform SMBus transaction
// I2C_FUNC capability bits // I2C_FUNC capability bits
i2cFuncSmbusQuick = 0x00010000 i2cFuncSmbusQuick = 0x00010000
i2cFuncSmbusReadByte = 0x00020000 i2cFuncSmbusReadByte = 0x00020000
// SMBus transaction types // SMBus transaction types
i2cSmbusRead = 0 i2cSmbusRead = 0
i2cSmbusWrite = 1 i2cSmbusWrite = 1
// SMBus protocol sizes // SMBus protocol sizes
i2cSmbusQuick = 0 i2cSmbusQuick = 0
i2cSmbusByte = 1 i2cSmbusByte = 1
) )
// i2cSmbusData matches the kernel union i2c_smbus_data (34 bytes max). // i2cSmbusData matches the kernel union i2c_smbus_data (34 bytes max).
// For quick and byte transactions only the first byte is used (if at all). // For quick and byte transactions only the first byte is used (if at all).
type i2cSmbusData [34]byte type i2cSmbusData [34]byte
// i2cSmbusArgs matches the kernel struct i2c_smbus_ioctl_data. // i2cSmbusArgs matches the kernel struct i2c_smbus_ioctl_data.
type i2cSmbusArgs struct { type i2cSmbusArgs struct {
readWrite uint8 readWrite uint8
command uint8 command uint8
size uint32 size uint32
data *i2cSmbusData data *i2cSmbusData
} }
// smbusProbe performs a single SMBus probe at the given address. // smbusProbe performs a single SMBus probe at the given address.
// Uses SMBus Quick Write (safest) or falls back to SMBus Read Byte for // Uses SMBus Quick Write (safest) or falls back to SMBus Read Byte for
// EEPROM address ranges where quick write can corrupt AT24RF08 chips. // EEPROM address ranges where quick write can corrupt AT24RF08 chips.
// This matches i2cdetect's MODE_AUTO behavior. // This matches i2cdetect's MODE_AUTO behavior.
func smbusProbe(fd int, addr int, hasQuick bool) bool { func smbusProbe(fd int, addr int, hasQuick bool) bool {
// EEPROM ranges: use read byte (quick write can corrupt AT24RF08) // EEPROM ranges: use read byte (quick write can corrupt AT24RF08)
useReadByte := (addr >= 0x30 && addr <= 0x37) || (addr >= 0x50 && addr <= 0x5F) useReadByte := (addr >= 0x30 && addr <= 0x37) || (addr >= 0x50 && addr <= 0x5F)
if !useReadByte && hasQuick { if !useReadByte && hasQuick {
// SMBus Quick Write: [START] [ADDR|W] [ACK/NACK] [STOP] // SMBus Quick Write: [START] [ADDR|W] [ACK/NACK] [STOP]
// Safest probe — no data transferred // Safest probe — no data transferred
args := i2cSmbusArgs{ args := i2cSmbusArgs{
readWrite: i2cSmbusWrite, readWrite: i2cSmbusWrite,
command: 0, command: 0,
size: i2cSmbusQuick, size: i2cSmbusQuick,
data: nil, data: nil,
} }
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args))) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args)))
return errno == 0 return errno == 0
} }
// SMBus Read Byte: [START] [ADDR|R] [ACK/NACK] [DATA] [STOP] // SMBus Read Byte: [START] [ADDR|R] [ACK/NACK] [DATA] [STOP]
var data i2cSmbusData var data i2cSmbusData
args := i2cSmbusArgs{ args := i2cSmbusArgs{
readWrite: i2cSmbusRead, readWrite: i2cSmbusRead,
command: 0, command: 0,
size: i2cSmbusByte, size: i2cSmbusByte,
data: &data, data: &data,
} }
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args))) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args)))
return errno == 0 return errno == 0
} }
// scan probes valid 7-bit addresses on a bus for connected devices. // scan probes valid 7-bit addresses on a bus for connected devices.
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO: // Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges. // SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
func (t *I2CTool) scan(args map[string]any) *ToolResult { func (t *I2CTool) scan(args map[string]any) *ToolResult {
bus, errResult := parseI2CBus(args) bus, errResult := parseI2CBus(args)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
devPath := fmt.Sprintf("/dev/i2c-%s", bus) devPath := fmt.Sprintf("/dev/i2c-%s", bus)
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and i2c-dev module)", devPath, err)) return ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and i2c-dev module)", devPath, err))
} }
defer syscall.Close(fd) defer syscall.Close(fd)
// Query adapter capabilities to determine available probe methods. // Query adapter capabilities to determine available probe methods.
// I2C_FUNCS writes an unsigned long, which is word-sized on Linux. // I2C_FUNCS writes an unsigned long, which is word-sized on Linux.
var funcs uintptr var funcs uintptr
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cFuncs, uintptr(unsafe.Pointer(&funcs))) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cFuncs, uintptr(unsafe.Pointer(&funcs)))
if errno != 0 { if errno != 0 {
return ErrorResult(fmt.Sprintf("failed to query I2C adapter capabilities on %s: %v", devPath, errno)) return ErrorResult(fmt.Sprintf("failed to query I2C adapter capabilities on %s: %v", devPath, errno))
} }
hasQuick := funcs&i2cFuncSmbusQuick != 0 hasQuick := funcs&i2cFuncSmbusQuick != 0
hasReadByte := funcs&i2cFuncSmbusReadByte != 0 hasReadByte := funcs&i2cFuncSmbusReadByte != 0
if !hasQuick && !hasReadByte { if !hasQuick && !hasReadByte {
return ErrorResult( return ErrorResult(
fmt.Sprintf(
fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath), "I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely",
devPath,
),
) )
} }
type deviceEntry struct { type deviceEntry struct {
Address string `json:"address"` Address string `json:"address"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
} }
var found []deviceEntry var found []deviceEntry
// Scan 0x08-0x77, skipping I2C reserved addresses 0x00-0x07 // Scan 0x08-0x77, skipping I2C reserved addresses 0x00-0x07
for addr := 0x08; addr <= 0x77; addr++ { for addr := 0x08; addr <= 0x77; addr++ {
// Set slave address — EBUSY means a kernel driver owns this address // Set slave address — EBUSY means a kernel driver owns this address
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr)) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr))
if errno != 0 { if errno != 0 {
if errno == syscall.EBUSY { if errno == syscall.EBUSY {
found = append(found, deviceEntry{ found = append(found, deviceEntry{
Address: fmt.Sprintf("0x%02x", addr), Address: fmt.Sprintf("0x%02x", addr),
Status: "busy (in use by kernel driver)", Status: "busy (in use by kernel driver)",
}) })
} }
continue continue
} }
if smbusProbe(fd, addr, hasQuick) { if smbusProbe(fd, addr, hasQuick) {
found = append(found, deviceEntry{ found = append(found, deviceEntry{
Address: fmt.Sprintf("0x%02x", addr), Address: fmt.Sprintf("0x%02x", addr),
}) })
} }
} }
if len(found) == 0 { if len(found) == 0 {
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath)) return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
} }
result, _ := json.MarshalIndent(map[string]any{ result, _ := json.MarshalIndent(map[string]any{
"bus": devPath, "bus": devPath,
"devices": found, "devices": found,
"count": len(found), "count": len(found),
}, "", " ") }, "", " ")
return SilentResult(fmt.Sprintf("Scan of %s:\n%s", devPath, string(result))) return SilentResult(fmt.Sprintf("Scan of %s:\n%s", devPath, string(result)))
} }
// readDevice reads bytes from an I2C device, optionally at a specific register // readDevice reads bytes from an I2C device, optionally at a specific register.
func (t *I2CTool) readDevice(args map[string]any) *ToolResult { func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
bus, errResult := parseI2CBus(args) bus, errResult := parseI2CBus(args)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
addr, errResult := parseI2CAddress(args) addr, errResult := parseI2CAddress(args)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
length := 1 length := 1
if l, ok := args["length"].(float64); ok { if l, ok := args["length"].(float64); ok {
length = int(l) length = int(l)
} }
if length < 1 || length > 256 { if length < 1 || length > 256 {
return ErrorResult("length must be between 1 and 256") return ErrorResult("length must be between 1 and 256")
} }
devPath := fmt.Sprintf("/dev/i2c-%s", bus) devPath := fmt.Sprintf("/dev/i2c-%s", bus)
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err)) return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err))
} }
defer syscall.Close(fd) defer syscall.Close(fd)
// Set slave address // Set slave address
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr)) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr))
if errno != 0 { if errno != 0 {
return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno)) return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno))
} }
// If register is specified, write it first // If register is specified, write it first
if regFloat, ok := args["register"].(float64); ok { if regFloat, ok := args["register"].(float64); ok {
reg := int(regFloat) reg := int(regFloat)
if reg < 0 || reg > 255 { if reg < 0 || reg > 255 {
return ErrorResult("register must be between 0x00 and 0xFF") return ErrorResult("register must be between 0x00 and 0xFF")
} }
_, err = syscall.Write(fd, []byte{byte(reg)}) _, err = syscall.Write(fd, []byte{byte(reg)})
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to write register 0x%02x: %v", reg, err)) return ErrorResult(fmt.Sprintf("failed to write register 0x%02x: %v", reg, err))
} }
} }
// Read data // Read data
buf := make([]byte, length) buf := make([]byte, length)
n, err := syscall.Read(fd, buf) n, err := syscall.Read(fd, buf)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to read from device 0x%02x: %v", addr, err)) return ErrorResult(fmt.Sprintf("failed to read from device 0x%02x: %v", addr, err))
} }
// Format as hex bytes // Format as hex bytes
hexBytes := make([]string, n) hexBytes := make([]string, n)
intBytes := make([]int, n) intBytes := make([]int, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
hexBytes[i] = fmt.Sprintf("0x%02x", buf[i]) hexBytes[i] = fmt.Sprintf("0x%02x", buf[i])
intBytes[i] = int(buf[i]) intBytes[i] = int(buf[i])
} }
result, _ := json.MarshalIndent(map[string]any{ result, _ := json.MarshalIndent(map[string]any{
"bus": devPath, "bus": devPath,
"address": fmt.Sprintf("0x%02x", addr), "address": fmt.Sprintf("0x%02x", addr),
"bytes": intBytes, "bytes": intBytes,
"hex": hexBytes, "hex": hexBytes,
"length": n, "length": n,
}, "", " ") }, "", " ")
return SilentResult(string(result)) return SilentResult(string(result))
} }
// writeDevice writes bytes to an I2C device, optionally at a specific register // writeDevice writes bytes to an I2C device, optionally at a specific register.
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult { func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool) confirm, _ := args["confirm"].(bool)
if !confirm { if !confirm {
return ErrorResult( return ErrorResult(
"write operations require confirm: true." +
"write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.", " Please confirm with the user before writing to I2C devices," +
" as incorrect writes can misconfigure hardware.",
) )
} }
bus, errResult := parseI2CBus(args) bus, errResult := parseI2CBus(args)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
addr, errResult := parseI2CAddress(args) addr, errResult := parseI2CAddress(args)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
dataRaw, ok := args["data"].([]any) dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 { if !ok || len(dataRaw) == 0 {
return ErrorResult("data is required for write (array of byte values 0-255)") return ErrorResult("data is required for write (array of byte values 0-255)")
} }
if len(dataRaw) > 256 { if len(dataRaw) > 256 {
return ErrorResult("data too long: maximum 256 bytes per I2C transaction") return ErrorResult("data too long: maximum 256 bytes per I2C transaction")
} }
data := make([]byte, 0, len(dataRaw)+1) data := make([]byte, 0, len(dataRaw)+1)
// If register is specified, prepend it to the data // If register is specified, prepend it to the data
if regFloat, ok := args["register"].(float64); ok { if regFloat, ok := args["register"].(float64); ok {
reg := int(regFloat) reg := int(regFloat)
if reg < 0 || reg > 255 { if reg < 0 || reg > 255 {
return ErrorResult("register must be between 0x00 and 0xFF") return ErrorResult("register must be between 0x00 and 0xFF")
} }
data = append(data, byte(reg)) data = append(data, byte(reg))
} }
for i, v := range dataRaw { for i, v := range dataRaw {
f, ok := v.(float64) f, ok := v.(float64)
if !ok { if !ok {
return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i)) return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i))
} }
b := int(f) b := int(f)
if b < 0 || b > 255 { if b < 0 || b > 255 {
return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b)) return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b))
} }
data = append(data, byte(b)) data = append(data, byte(b))
} }
devPath := fmt.Sprintf("/dev/i2c-%s", bus) devPath := fmt.Sprintf("/dev/i2c-%s", bus)
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err)) return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err))
} }
defer syscall.Close(fd) defer syscall.Close(fd)
// Set slave address // Set slave address
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr)) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr))
if errno != 0 { if errno != 0 {
return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno)) return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno))
} }
// Write data // Write data
n, err := syscall.Write(fd, data) n, err := syscall.Write(fd, data)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to write to device 0x%02x: %v", addr, err)) return ErrorResult(fmt.Sprintf("failed to write to device 0x%02x: %v", addr, err))
} }
return SilentResult(fmt.Sprintf("Wrote %d byte(s) to device 0x%02x on %s", n, addr, devPath)) return SilentResult(fmt.Sprintf("Wrote %d byte(s) to device 0x%02x on %s", n, addr, devPath))
} }

View file

@ -11,129 +11,76 @@ import (
) )
func prepareCommandForTermination(cmd *exec.Cmd) { func prepareCommandForTermination(cmd *exec.Cmd) {
if cmd == nil { if cmd == nil {
return return
} }
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
} }
func terminateProcessTree(cmd *exec.Cmd) error { func terminateProcessTree(cmd *exec.Cmd) error {
if cmd == nil || cmd.Process == nil { if cmd == nil || cmd.Process == nil {
return nil return nil
} }
pid := cmd.Process.Pid pid := cmd.Process.Pid
if pid <= 0 { if pid <= 0 {
return nil return nil
} }
// Kill the entire process group spawned by the shell command. // Kill the entire process group spawned by the shell command.
_ = syscall.Kill(-pid, syscall.SIGKILL) _ = syscall.Kill(-pid, syscall.SIGKILL)
// Some shells/background jobs may still leave descendants around // Some shells/background jobs may still leave descendants around
// briefly; aggressively walk /proc and kill child processes too. // briefly; aggressively walk /proc and kill child processes too.
killDescendants(pid) killDescendants(pid)
// Fallback kill on the shell process itself. // Fallback kill on the shell process itself.
_ = cmd.Process.Kill() _ = cmd.Process.Kill()
return nil return nil
} }
func killDescendants(ppid int) { func killDescendants(ppid int) {
if ppid <= 0 { if ppid <= 0 {
return return
} }
entries, err := os.ReadDir("/proc") entries, err := os.ReadDir("/proc")
if err != nil { if err != nil {
return return
} }
for _, e := range entries { for _, e := range entries {
if !e.IsDir() { if !e.IsDir() {
continue continue
} }
childPID, err := strconv.Atoi(e.Name()) childPID, err := strconv.Atoi(e.Name())
if err != nil || childPID <= 0 || childPID == ppid { if err != nil || childPID <= 0 || childPID == ppid {
continue continue
} }
statPath := "/proc/" + e.Name() + "/stat" statPath := "/proc/" + e.Name() + "/stat"
data, err := os.ReadFile(statPath) data, err := os.ReadFile(statPath)
if err != nil { if err != nil {
continue continue
} }
// /proc/<pid>/stat: pid (comm) state ppid ... // /proc/<pid>/stat: pid (comm) state ppid ...
raw := string(data) raw := string(data)
end := strings.LastIndex(raw, ")") end := strings.LastIndex(raw, ")")
if end == -1 || end+2 >= len(raw) { if end == -1 || end+2 >= len(raw) {
continue continue
} }
fields := strings.Fields(raw[end+2:]) fields := strings.Fields(raw[end+2:])
if len(fields) < 2 { if len(fields) < 2 {
continue continue
} }
parent, err := strconv.Atoi(fields[1]) parent, err := strconv.Atoi(fields[1])
if err != nil || parent != ppid { if err != nil || parent != ppid {
continue continue
} }
// Recurse first, then kill child process/group. // Recurse first, then kill child process/group.
killDescendants(childPID) killDescendants(childPID)
_ = syscall.Kill(-childPID, syscall.SIGKILL) _ = syscall.Kill(-childPID, syscall.SIGKILL)
_ = syscall.Kill(childPID, syscall.SIGKILL) _ = syscall.Kill(childPID, syscall.SIGKILL)
} }
} }

View file

@ -14,122 +14,69 @@ import (
) )
func processRunning(pid int) bool { func processRunning(pid int) bool {
if pid <= 0 { if pid <= 0 {
return false return false
} }
// kill(0) can return success for zombie processes too, so inspect /proc // kill(0) can return success for zombie processes too, so inspect /proc
// state and treat zombies as not-running for timeout cleanup assertions. // state and treat zombies as not-running for timeout cleanup assertions.
err := syscall.Kill(pid, 0) err := syscall.Kill(pid, 0)
if err != nil && err != syscall.EPERM { if err != nil && err != syscall.EPERM {
return false return false
} }
data, readErr := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat") data, readErr := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
if readErr != nil { if readErr != nil {
return false return false
} }
raw := string(data) raw := string(data)
end := strings.LastIndex(raw, ")") end := strings.LastIndex(raw, ")")
if end == -1 || end+2 >= len(raw) { if end == -1 || end+2 >= len(raw) {
return true // best effort fallback return true // best effort fallback
} }
fields := strings.Fields(raw[end+2:]) fields := strings.Fields(raw[end+2:])
if len(fields) == 0 { if len(fields) == 0 {
return true // best effort fallback return true // best effort fallback
} }
state := fields[0] state := fields[0]
return state != "Z" return state != "Z"
} }
func TestShellTool_TimeoutKillsChildProcess(t *testing.T) { func TestShellTool_TimeoutKillsChildProcess(t *testing.T) {
tool, err := NewExecTool(t.TempDir(), false) tool, err := NewExecTool(t.TempDir(), false)
if err != nil { if err != nil {
t.Errorf("unable to configure exec tool: %s", err) t.Errorf("unable to configure exec tool: %s", err)
} }
tool.SetTimeout(500 * time.Millisecond) tool.SetTimeout(500 * time.Millisecond)
args := map[string]any{ args := map[string]any{
// Spawn a child process that would outlive the shell unless process-group kill is used. // Spawn a child process that would outlive the shell unless process-group kill is used.
"command": "sleep 60 & echo $! > child.pid; wait", "command": "sleep 60 & echo $! > child.pid; wait",
} }
result := tool.Execute(context.Background(), args) result := tool.Execute(context.Background(), args)
if !result.IsError { if !result.IsError {
t.Fatalf("expected timeout error, got success: %s", result.ForLLM) t.Fatalf("expected timeout error, got success: %s", result.ForLLM)
} }
if !strings.Contains(result.ForLLM, "timed out") { if !strings.Contains(result.ForLLM, "timed out") {
t.Fatalf("expected timeout message, got: %s", result.ForLLM) t.Fatalf("expected timeout message, got: %s", result.ForLLM)
} }
childPIDPath := filepath.Join(tool.workingDir, "child.pid") childPIDPath := filepath.Join(tool.workingDir, "child.pid")
data, err := os.ReadFile(childPIDPath) data, err := os.ReadFile(childPIDPath)
if err != nil { if err != nil {
t.Fatalf("failed to read child pid file: %v", err) t.Fatalf("failed to read child pid file: %v", err)
} }
childPID, err := strconv.Atoi(strings.TrimSpace(string(data))) childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil { if err != nil {
t.Fatalf("failed to parse child pid: %v", err) t.Fatalf("failed to parse child pid: %v", err)
} }
deadline := time.Now().Add(2 * time.Second) deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) { for time.Now().Before(deadline) {
if !processRunning(childPID) { if !processRunning(childPID) {
return return
} }
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
} }
t.Fatalf("child process %d is still running after timeout", childPID) t.Fatalf("child process %d is still running after timeout", childPID)
} }

View file

@ -9,321 +9,193 @@ import (
) )
// SPI ioctl constants from Linux kernel headers. // SPI ioctl constants from Linux kernel headers.
// Calculated from _IOW('k', nr, size) macro: // Calculated from _IOW('k', nr, size) macro:
// //
// direction(1)<<30 | size<<16 | type(0x6B)<<8 | nr // direction(1)<<30 | size<<16 | type(0x6B)<<8 | nr
const ( const (
spiIocWrMode = 0x40016B01 // _IOW('k', 1, __u8) spiIocWrMode = 0x40016B01 // _IOW('k', 1, __u8)
spiIocWrBitsPerWord = 0x40016B03 // _IOW('k', 3, __u8) spiIocWrBitsPerWord = 0x40016B03 // _IOW('k', 3, __u8)
spiIocWrMaxSpeedHz = 0x40046B04 // _IOW('k', 4, __u32) spiIocWrMaxSpeedHz = 0x40046B04 // _IOW('k', 4, __u32)
spiIocMessage1 = 0x40206B00 // _IOW('k', 0, struct spi_ioc_transfer) — 32 bytes spiIocMessage1 = 0x40206B00 // _IOW('k', 0, struct spi_ioc_transfer) — 32 bytes
) )
// spiTransfer matches Linux kernel struct spi_ioc_transfer (32 bytes on all architectures). // spiTransfer matches Linux kernel struct spi_ioc_transfer (32 bytes on all architectures).
type spiTransfer struct { type spiTransfer struct {
txBuf uint64 txBuf uint64
rxBuf uint64 rxBuf uint64
length uint32 length uint32
speedHz uint32 speedHz uint32
delayUsecs uint16 delayUsecs uint16
bitsPerWord uint8 bitsPerWord uint8
csChange uint8 csChange uint8
txNbits uint8 txNbits uint8
rxNbits uint8 rxNbits uint8
wordDelay uint8 wordDelay uint8
pad uint8 pad uint8
} }
// configureSPI opens an SPI device and sets mode, bits per word, and speed // configureSPI opens an SPI device and sets mode, bits per word, and speed.
func configureSPI(
func configureSPI(devPath string, mode uint8, bits uint8, speed uint32) (int, *ToolResult) { devPath string, mode uint8, bits uint8, speed uint32,
) (int, *ToolResult) {
fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0)
if err != nil { if err != nil {
return -1, ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and spidev module)", devPath, err)) return -1, ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and spidev module)", devPath, err))
} }
// Set SPI mode // Set SPI mode
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrMode, uintptr(unsafe.Pointer(&mode))) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrMode, uintptr(unsafe.Pointer(&mode)))
if errno != 0 { if errno != 0 {
syscall.Close(fd) syscall.Close(fd)
return -1, ErrorResult(fmt.Sprintf("failed to set SPI mode %d: %v", mode, errno)) return -1, ErrorResult(fmt.Sprintf("failed to set SPI mode %d: %v", mode, errno))
} }
// Set bits per word // Set bits per word
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrBitsPerWord, uintptr(unsafe.Pointer(&bits))) _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrBitsPerWord, uintptr(unsafe.Pointer(&bits)))
if errno != 0 { if errno != 0 {
syscall.Close(fd) syscall.Close(fd)
return -1, ErrorResult(fmt.Sprintf("failed to set bits per word %d: %v", bits, errno)) return -1, ErrorResult(fmt.Sprintf("failed to set bits per word %d: %v", bits, errno))
} }
// Set max speed // Set max speed
_, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrMaxSpeedHz, uintptr(unsafe.Pointer(&speed))) _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocWrMaxSpeedHz, uintptr(unsafe.Pointer(&speed)))
if errno != 0 { if errno != 0 {
syscall.Close(fd) syscall.Close(fd)
return -1, ErrorResult(fmt.Sprintf("failed to set SPI speed %d Hz: %v", speed, errno)) return -1, ErrorResult(fmt.Sprintf("failed to set SPI speed %d Hz: %v", speed, errno))
} }
return fd, nil return fd, nil
} }
// transfer performs a full-duplex SPI transfer // transfer performs a full-duplex SPI transfer.
func (t *SPITool) transfer(args map[string]any) *ToolResult { func (t *SPITool) transfer(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool) confirm, _ := args["confirm"].(bool)
if !confirm { if !confirm {
return ErrorResult( return ErrorResult(
"transfer operations require confirm: true." +
"transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.", " Please confirm with the user before sending data to SPI devices.",
) )
} }
dev, speed, mode, bits, errMsg := parseSPIArgs(args) dev, speed, mode, bits, errMsg := parseSPIArgs(args)
if errMsg != "" { if errMsg != "" {
return ErrorResult(errMsg) return ErrorResult(errMsg)
} }
dataRaw, ok := args["data"].([]any) dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 { if !ok || len(dataRaw) == 0 {
return ErrorResult("data is required for transfer (array of byte values 0-255)") return ErrorResult("data is required for transfer (array of byte values 0-255)")
} }
if len(dataRaw) > 4096 { if len(dataRaw) > 4096 {
return ErrorResult("data too long: maximum 4096 bytes per SPI transfer") return ErrorResult("data too long: maximum 4096 bytes per SPI transfer")
} }
txBuf := make([]byte, len(dataRaw)) txBuf := make([]byte, len(dataRaw))
for i, v := range dataRaw { for i, v := range dataRaw {
f, ok := v.(float64) f, ok := v.(float64)
if !ok { if !ok {
return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i)) return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i))
} }
b := int(f) b := int(f)
if b < 0 || b > 255 { if b < 0 || b > 255 {
return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b)) return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b))
} }
txBuf[i] = byte(b) txBuf[i] = byte(b)
} }
devPath := fmt.Sprintf("/dev/spidev%s", dev) devPath := fmt.Sprintf("/dev/spidev%s", dev)
fd, errResult := configureSPI(devPath, mode, bits, speed) fd, errResult := configureSPI(devPath, mode, bits, speed)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
defer syscall.Close(fd) defer syscall.Close(fd)
rxBuf := make([]byte, len(txBuf)) rxBuf := make([]byte, len(txBuf))
xfer := spiTransfer{ xfer := spiTransfer{
txBuf: uint64(uintptr(unsafe.Pointer(&txBuf[0]))), txBuf: uint64(uintptr(unsafe.Pointer(&txBuf[0]))),
rxBuf: uint64(uintptr(unsafe.Pointer(&rxBuf[0]))), rxBuf: uint64(uintptr(unsafe.Pointer(&rxBuf[0]))),
length: uint32(len(txBuf)), length: uint32(len(txBuf)),
speedHz: speed, speedHz: speed,
bitsPerWord: bits, bitsPerWord: bits,
} }
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocMessage1, uintptr(unsafe.Pointer(&xfer))) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocMessage1, uintptr(unsafe.Pointer(&xfer)))
runtime.KeepAlive(txBuf) runtime.KeepAlive(txBuf)
runtime.KeepAlive(rxBuf) runtime.KeepAlive(rxBuf)
if errno != 0 { if errno != 0 {
return ErrorResult(fmt.Sprintf("SPI transfer failed: %v", errno)) return ErrorResult(fmt.Sprintf("SPI transfer failed: %v", errno))
} }
// Format received bytes // Format received bytes
hexBytes := make([]string, len(rxBuf)) hexBytes := make([]string, len(rxBuf))
intBytes := make([]int, len(rxBuf)) intBytes := make([]int, len(rxBuf))
for i, b := range rxBuf { for i, b := range rxBuf {
hexBytes[i] = fmt.Sprintf("0x%02x", b) hexBytes[i] = fmt.Sprintf("0x%02x", b)
intBytes[i] = int(b) intBytes[i] = int(b)
} }
result, _ := json.MarshalIndent(map[string]any{ result, _ := json.MarshalIndent(map[string]any{
"device": devPath, "device": devPath,
"sent": len(txBuf), "sent": len(txBuf),
"received": intBytes, "received": intBytes,
"hex": hexBytes, "hex": hexBytes,
}, "", " ") }, "", " ")
return SilentResult(string(result)) return SilentResult(string(result))
} }
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed) // readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed).
func (t *SPITool) readDevice(args map[string]any) *ToolResult { func (t *SPITool) readDevice(args map[string]any) *ToolResult {
dev, speed, mode, bits, errMsg := parseSPIArgs(args) dev, speed, mode, bits, errMsg := parseSPIArgs(args)
if errMsg != "" { if errMsg != "" {
return ErrorResult(errMsg) return ErrorResult(errMsg)
} }
length := 0 length := 0
if l, ok := args["length"].(float64); ok { if l, ok := args["length"].(float64); ok {
length = int(l) length = int(l)
} }
if length < 1 || length > 4096 { if length < 1 || length > 4096 {
return ErrorResult("length is required for read (1-4096)") return ErrorResult("length is required for read (1-4096)")
} }
devPath := fmt.Sprintf("/dev/spidev%s", dev) devPath := fmt.Sprintf("/dev/spidev%s", dev)
fd, errResult := configureSPI(devPath, mode, bits, speed) fd, errResult := configureSPI(devPath, mode, bits, speed)
if errResult != nil { if errResult != nil {
return errResult return errResult
} }
defer syscall.Close(fd) defer syscall.Close(fd)
txBuf := make([]byte, length) // zeros txBuf := make([]byte, length) // zeros
rxBuf := make([]byte, length) rxBuf := make([]byte, length)
xfer := spiTransfer{ xfer := spiTransfer{
txBuf: uint64(uintptr(unsafe.Pointer(&txBuf[0]))), txBuf: uint64(uintptr(unsafe.Pointer(&txBuf[0]))),
rxBuf: uint64(uintptr(unsafe.Pointer(&rxBuf[0]))), rxBuf: uint64(uintptr(unsafe.Pointer(&rxBuf[0]))),
length: uint32(length), length: uint32(length),
speedHz: speed, speedHz: speed,
bitsPerWord: bits, bitsPerWord: bits,
} }
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocMessage1, uintptr(unsafe.Pointer(&xfer))) _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), spiIocMessage1, uintptr(unsafe.Pointer(&xfer)))
runtime.KeepAlive(txBuf) runtime.KeepAlive(txBuf)
runtime.KeepAlive(rxBuf) runtime.KeepAlive(rxBuf)
if errno != 0 { if errno != 0 {
return ErrorResult(fmt.Sprintf("SPI read failed: %v", errno)) return ErrorResult(fmt.Sprintf("SPI read failed: %v", errno))
} }
hexBytes := make([]string, len(rxBuf)) hexBytes := make([]string, len(rxBuf))
intBytes := make([]int, len(rxBuf)) intBytes := make([]int, len(rxBuf))
for i, b := range rxBuf { for i, b := range rxBuf {
hexBytes[i] = fmt.Sprintf("0x%02x", b) hexBytes[i] = fmt.Sprintf("0x%02x", b)
intBytes[i] = int(b) intBytes[i] = int(b)
} }
result, _ := json.MarshalIndent(map[string]any{ result, _ := json.MarshalIndent(map[string]any{
"device": devPath, "device": devPath,
"bytes": intBytes, "bytes": intBytes,
"hex": hexBytes, "hex": hexBytes,
"length": len(rxBuf), "length": len(rxBuf),
}, "", " ") }, "", " ")
return SilentResult(string(result)) return SilentResult(string(result))
} }