diff --git a/serial_setserial_linux.go b/serial_setserial_linux.go new file mode 100644 index 0000000..4571b53 --- /dev/null +++ b/serial_setserial_linux.go @@ -0,0 +1,78 @@ +//go:build linux +// +build linux + +package serial + +import ( + "fmt" + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +// CSerialStruct is a C-interop struct for linux/serial.h: struct serial_struct +// Field order and types must exactly match the C definition for binary compatibility. +// See: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/serial.h#L135 +type CSerialStruct struct { + Type int32 + Line int32 + Port uint32 + IRQ int32 + Flags int32 + XmitFifoSize int32 + CustomDivisor int32 + BaudBase int32 + CloseDelay uint16 + IOType byte + ReservedChar byte + Hub6 int32 + ClosingWait uint16 + ClosingWait2 uint16 + IOMemBase uintptr + IOMemRegShift uint16 + PortHigh uint32 + // IOMapBase: In C, this is 'unsigned long'. Use uint64 for 64-bit systems. + // For 32-bit systems, you may need to use uint32 for binary compatibility. + IOMapBase uint64 +} + +// GetSerialStruct opens the device and retrieves the CSerialStruct using TIOCGSERIAL ioctl. +func GetSerialStruct(device string) (*CSerialStruct, error) { + f, err := os.OpenFile(device, os.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0666) + if err != nil { + return nil, fmt.Errorf("failed to open device: %w", err) + } + defer f.Close() + + var ser CSerialStruct + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.TIOCGSERIAL, uintptr(unsafe.Pointer(&ser))) + if errno != 0 { + return nil, fmt.Errorf("ioctl TIOCGSERIAL failed: %v", errno) + } + return &ser, nil +} + +// SetSerialPortMode sets the port mode using ioctl TIOCSSERIAL +func SetSerialPortMode(device string, portMode uint32) error { + f, err := os.OpenFile(device, os.O_RDWR|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0666) + if err != nil { + return fmt.Errorf("failed to open device: %w", err) + } + defer f.Close() + + var ser CSerialStruct + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.TIOCGSERIAL, uintptr(unsafe.Pointer(&ser))) + if errno != 0 { + return fmt.Errorf("ioctl TIOCGSERIAL failed: %v", errno) + } + + ser.Port = portMode + + _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.TIOCSSERIAL, uintptr(unsafe.Pointer(&ser))) + if errno != 0 { + return fmt.Errorf("ioctl TIOCSSERIAL failed: %v", errno) + } + return nil +} diff --git a/serial_setserial_linux_test.go b/serial_setserial_linux_test.go new file mode 100644 index 0000000..aafb565 --- /dev/null +++ b/serial_setserial_linux_test.go @@ -0,0 +1,52 @@ +//go:build linux +// +build linux + +package serial + +import ( + "context" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +func startSocatAndWaitForSetserialTest(t *testing.T, ctx context.Context) *exec.Cmd { + cmd := exec.CommandContext(ctx, "socat", "-D", "STDIO", "pty,link=/tmp/faketty_setserial") + r, err := cmd.StderrPipe() + require.NoError(t, err) + require.NoError(t, cmd.Start()) + buf := make([]byte, 1024) + _, err = r.Read(buf) + require.NoError(t, err) + return cmd +} + +func TestGetSerialStructOnFakeTty(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cmd := startSocatAndWaitForSetserialTest(t, ctx) + go cmd.Wait() + + ser, err := GetSerialStruct("/tmp/faketty_setserial") + // Note: socat's virtual TTY may not fully support TIOCGSERIAL. + // If not supported, skip the test (environment dependent). + if err != nil { + t.Skipf("ioctl TIOCGSERIAL not supported on socat pty: %v", err) + } + require.NotNil(t, ser) +} + +func TestSetSerialPortModeOnFakeTty(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cmd := startSocatAndWaitForSetserialTest(t, ctx) + go cmd.Wait() + + err := SetSerialPortMode("/tmp/faketty_setserial", 0) + // Note: socat's virtual TTY may not fully support TIOCSSERIAL. + // If not supported, skip the test (environment dependent). + if err != nil { + t.Skipf("ioctl TIOCSSERIAL not supported on socat pty: %v", err) + } +}