GitHub Action中运行Go伪终端测试失败:read /dev/ptmx输入输出错误
问题:GitHub Action中Go测试使用pty库出现
read /dev/ptmx: input/output error错误 我在GitHub Action流水线中运行如下Go测试用例,该测试使用github.com/creack/pty库开启伪终端:
func TestMysqlConnect(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), connectTimeout) defer cancel() cmd := exec.CommandContext(ctx, "xxx", "connect", ...) t.Logf("About to run connect command %q", cmd.String()) ptmx, err := pty.Start(cmd) require.NoErrorf(t, err, "Failed to start pseudoterminal") defer func() { _ = ptmx.Close() }() go func() { ptmx.Write([]byte("SELECT 1;\n")) ptmx.Write([]byte{4}) }() var buff bytes.Buffer _, err = io.Copy(&buff, ptmx) require.NoError(t, err) require.Contains(t, buff.String(), "1 row in set") }
其中xxx是一个CLI命令,本质是通过SSH连接到目标机器并打开MySQL CLI。该测试在本地Apple M2环境可正常运行,但在使用Ubuntu 22.04镜像的GitHub Action中执行时,io.Copy操作抛出错误:read /dev/ptmx: input/output error。
我尝试提取错误信息,发现其类型为fs.PathError,仅包含OP=read、Path=/dev/ptmx、Err=input/output字段,无法获取更多调试信息。此外我已尝试使用shell: 'script -q -e -c "bash {0}"'执行测试,但仍出现相同错误:
- name: run integration test shell: 'script -q -e -c "bash {0}"' run: go test -run Test ./services/some-service -tags=integration
补充说明:
- 无法控制
xxx命令的内部逻辑,无法修改其中的SSH命令添加-t参数; /dev/ptmx文件权限为Dcrw-rw-rw-,看起来是正确的。
请问该问题的原因是什么?或者如何获取更多错误细节来调试?
可能的原因及调试方案
可能原因
- GitHub Action环境PTY限制:GitHub Action的Ubuntu runner是容器化环境,对伪终端的支持可能存在限制,或者默认终端配置(如
TERM环境变量)与本地M2环境不一致。当xxx命令通过SSH连接时,即使外部开启了PTY,SSH会话内部可能因为缺少终端交互所需的环境变量,导致MySQL CLI或SSH进程异常退出,进而引发/dev/ptmx的IO错误。 - 进程退出时机冲突:测试中goroutine写入SQL命令和EOF(
\x04)后,xxx命令或MySQL CLI可能快速退出,此时io.Copy尝试从已关闭的PTMX设备读取,就会触发IO错误。本地环境因进程调度、终端处理速度差异,未触发该问题。 - PTY设备生命周期异常:容器化环境中PTY设备的创建、销毁机制与本地不同,当子进程
xxx意外终止时,PTMX设备状态未被正确清理,导致后续读取操作报错。
调试方案
1. 监控子进程状态
在测试中添加对cmd进程的状态监控,明确进程退出原因和退出码:
go func() { err := cmd.Wait() if err != nil { t.Logf("Command exited with error: %v, exit code: %d", err, cmd.ProcessState.ExitCode()) } else { t.Logf("Command exited successfully, exit code: %d", cmd.ProcessState.ExitCode()) } }()
2. 捕获完整输出(含stderr)
默认exec.Command的stderr未重定向到PTY,可能错过xxx命令或SSH的错误日志,可将stderr合并到stdout:
cmd.Stderr = cmd.Stdout
这样buff中会包含所有输出,便于定位xxx内部的问题。
3. 模拟本地环境变量
本地与GitHub Action的环境变量(如TERM)可能存在差异,测试时手动设置终端类型:
cmd.Env = append(os.Environ(), "TERM=xterm")
4. 调整IO错误处理逻辑
如果是进程正常退出导致的读取错误,可针对性忽略,同时保留输出检查:
_, err = io.Copy(&buff, ptmx) // 忽略进程退出引发的IO错误,仅检查关键输出 if err != nil { t.Logf("IO copy error: %v", err) } require.Contains(t, buff.String(), "1 row in set")
5. 检查runner的PTY基础支持
在流水线中添加步骤,验证runner环境的PTY功能:
- name: Check PTY support run: | echo "TERM: $TERM" ls -l /dev/ptmx script -q -c "echo 'PTY test passed'" /dev/null
内容的提问来源于stack exchange,提问作者Avishay28
相关产品推荐
相关产品推荐

