feat: make pseudo-TTY allocation opt-in (#961)

Fixes #956.

Pseudo-TTY allocation is now an explicit, runner-wide opt-in via `runner.allocate_pty`, applied to both host and docker backends. Default is off, matching GitHub `actions/runner`.

```yaml
runner:
  allocate_pty: false  # default
```

**Before:** the host backend hardcoded `if true /* allocate Terminal */` and the docker backend used `term.IsTerminal(os.Stdout.Fd())`. As a result, `docker build` (and other TTY-aware tools) saw a TTY and emitted cursor-control redraw frames that flooded captured logs with thousands of duplicate-looking progress lines — only on host-mode runners in production, and on docker-mode runners when the daemon happened to be launched from a shell rather than a service.

**After:** both backends consult `Config.AllocatePTY`. The `term.IsTerminal` heuristic is gone, so behavior no longer depends on whether the daemon has a controlling terminal.

**Reproduction:** running `docker build` through `HostEnvironment.Exec` with output captured to a buffer:

| | Before (`if true`) | After (`AllocatePTY=false`) |
|---|---:|---:|
| bytes captured | 18,167 | 1,048 |
| ANSI CSI sequences | 556 | 0 |
| cursor-up `\e[1A` | 181 | 0 |

**Side fix:** `ptyWriter.AutoStop` is now `atomic.Bool`. The field is written from the exec goroutine after `cmd.Wait()` and read from the `copyPtyOutput` goroutine via `ptyWriter.Write`; existing tests never tripped the race detector because their commands produced no output before exit. The new host-mode test does.

---
This PR was written with the help of Claude Opus 4.7

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/961
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-committed-by: silverwind <2021+silverwind@noreply.gitea.com>
This commit is contained in:
silverwind
2026-05-15 18:11:39 +00:00
committed by Lunny Xiao
parent 880e9755d9
commit 3c5f03ff8f
12 changed files with 120 additions and 20 deletions

View File

@@ -19,6 +19,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"gitea.com/gitea/runner/act/common"
@@ -42,6 +43,7 @@ type HostEnvironment struct {
ActPath string
CleanUp func()
StdOut io.Writer
AllocatePTY bool // allocate a pseudo-TTY for each step's process
mu sync.Mutex
runningPIDs map[int]struct{}
@@ -200,12 +202,12 @@ func (e *HostEnvironment) Start(_ bool) common.Executor {
type ptyWriter struct {
Out io.Writer
AutoStop bool
AutoStop atomic.Bool
dirtyLine bool
}
func (w *ptyWriter) Write(buf []byte) (int, error) {
if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
if w.AutoStop.Load() && len(buf) > 0 && buf[len(buf)-1] == 4 {
n, err := w.Out.Write(buf[:len(buf)-1])
if err != nil {
return n, err
@@ -335,21 +337,20 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
tty.Close()
}
}()
if true /* allocate Terminal */ {
if e.AllocatePTY {
var err error
ppty, tty, err = setupPty(cmd, cmdline)
if err != nil {
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
}
}
writer := &ptyWriter{Out: e.StdOut}
logctx, finishLog := context.WithCancel(context.Background())
var writer *ptyWriter
var logctx context.Context
if ppty != nil {
writer = &ptyWriter{Out: e.StdOut}
var finishLog context.CancelFunc
logctx, finishLog = context.WithCancel(context.Background())
go copyPtyOutput(writer, ppty, finishLog)
} else {
finishLog()
}
if ppty != nil {
go writeKeepAlive(ppty)
}
// Split Start/Wait so the PID can be registered before the process can exit;
@@ -379,14 +380,11 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
return err
}
if tty != nil {
writer.AutoStop = true
writer.AutoStop.Store(true)
if _, err := tty.WriteString("\x04"); err != nil {
common.Logger(ctx).Debug("Failed to write EOT")
}
}
<-logctx.Done()
if ppty != nil {
<-logctx.Done()
ppty.Close()
ppty = nil
}