Skip to content

Commit ef88242

Browse files
add --no-tty flag for non-interactive runs
1 parent 82c0d9f commit ef88242

8 files changed

Lines changed: 198 additions & 71 deletions

File tree

‎backend/backend.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package backend
22

33
import (
44
"context"
5+
"io"
56
)
67

78
// Backend defines the interface for container/VM backends
@@ -117,4 +118,12 @@ type RunOptions struct {
117118
// before mounts are set up, to prevent image defaults from conflicting
118119
// with host configs provided via mounts.
119120
CleanMountPaths []string
121+
122+
// NoTTY disables terminal allocation and interactive terminal handling.
123+
NoTTY bool
124+
125+
// Stdin, Stdout, and Stderr are the streams used for the tool process.
126+
Stdin io.Reader
127+
Stdout io.Writer
128+
Stderr io.Writer
120129
}

‎backend/container/container.go‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ func (c *Client) Build(ctx context.Context, opts backend.BuildOptions) (string,
178178

179179
// Run runs a container using the container CLI.
180180
func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
181+
stdout := opts.Stdout
182+
if stdout == nil {
183+
stdout = io.Discard
184+
}
185+
stderr := opts.Stderr
186+
if stderr == nil {
187+
stderr = io.Discard
188+
}
189+
181190
// Append Docker daemon startup hook so mount-wait and other hooks run first.
182191
// dockerd is already backgrounded (& in the hook) so it doesn't block.
183192
opts.PreRunHooks = append(opts.PreRunHooks, dockerStartHook)
@@ -212,7 +221,9 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
212221
args := []string{"run",
213222
"--rm",
214223
"-i",
215-
"-t",
224+
}
225+
if !opts.NoTTY {
226+
args = append(args, "-t")
216227
}
217228
args = append(args, resourceArgs()...)
218229

@@ -364,6 +375,36 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
364375

365376
cmd := exec.Command("container", args...)
366377

378+
if opts.NoTTY {
379+
cmd.Stdin = opts.Stdin
380+
cmd.Stdout = stdout
381+
cmd.Stderr = stderr
382+
383+
sigCh := make(chan os.Signal, 1)
384+
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
385+
defer signal.Stop(sigCh)
386+
387+
go func() {
388+
select {
389+
case <-sigCh:
390+
case <-ctx.Done():
391+
}
392+
if opts.Name != "" {
393+
exec.Command("container", "rm", "-f", opts.Name).Run()
394+
}
395+
}()
396+
397+
waitErr := cmd.Run()
398+
if waitErr != nil {
399+
if exitErr, ok := waitErr.(*exec.ExitError); ok {
400+
return fmt.Errorf("container exited with status %d", exitErr.ExitCode())
401+
}
402+
return fmt.Errorf("container error: %w", waitErr)
403+
}
404+
405+
return nil
406+
}
407+
367408
// Save terminal state and ensure it's restored on exit
368409
fd := int(os.Stdin.Fd())
369410
oldState, _ := unix.IoctlGetTermios(fd, unix.TIOCGETA)
@@ -431,7 +472,7 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
431472

432473
// Copy container output to stdout
433474
go func() {
434-
io.Copy(os.Stdout, ptmx)
475+
io.Copy(stdout, ptmx)
435476
}()
436477

437478
// Copy stdin to container, intercepting triple Ctrl-C to kill

‎backend/docker/docker.go‎

Lines changed: 81 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/docker/docker/api/types/container"
2020
"github.com/docker/docker/api/types/mount"
2121
"github.com/docker/docker/client"
22+
"github.com/docker/docker/pkg/stdcopy"
2223
"github.com/docker/go-connections/nat"
2324
"github.com/kballard/go-shellquote"
2425
"github.com/leighmcculloch/silo/backend" // parent package
@@ -149,6 +150,15 @@ func (c *Client) Build(ctx context.Context, opts backend.BuildOptions) (string,
149150

150151
// Run runs a container with the given options
151152
func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
153+
stdout := opts.Stdout
154+
if stdout == nil {
155+
stdout = io.Discard
156+
}
157+
stderr := opts.Stderr
158+
if stderr == nil {
159+
stderr = io.Discard
160+
}
161+
152162
// Convert mounts
153163
var mounts []mount.Mount
154164
for _, m := range opts.MountsRO {
@@ -228,16 +238,17 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
228238
}
229239

230240
// Create container configuration
241+
attachStdin := !opts.NoTTY || opts.Stdin != nil
231242
config := &container.Config{
232243
Image: opts.Image,
233244
WorkingDir: opts.WorkDir,
234245
Env: opts.Env,
235246
Entrypoint: entrypoint,
236247
Cmd: cmd,
237-
Tty: true,
238-
OpenStdin: true,
239-
StdinOnce: true,
240-
AttachStdin: true,
248+
Tty: !opts.NoTTY,
249+
OpenStdin: attachStdin,
250+
StdinOnce: attachStdin,
251+
AttachStdin: attachStdin,
241252
AttachStdout: true,
242253
AttachStderr: true,
243254
ExposedPorts: exposedPorts,
@@ -263,7 +274,7 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
263274
// Attach to the container
264275
attachResp, err := c.cli.ContainerAttach(ctx, resp.ID, container.AttachOptions{
265276
Stream: true,
266-
Stdin: true,
277+
Stdin: attachStdin,
267278
Stdout: true,
268279
Stderr: true,
269280
})
@@ -280,20 +291,22 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
280291
return fmt.Errorf("failed to start container: %w", err)
281292
}
282293

283-
// Set terminal to raw mode and handle resizing
284-
fd := os.Stdin.Fd()
285-
if term.IsTerminal(fd) {
286-
oldState, err := term.MakeRaw(fd)
287-
if err != nil {
288-
return fmt.Errorf("failed to set raw terminal: %w", err)
289-
}
290-
defer term.RestoreTerminal(fd, oldState)
294+
if !opts.NoTTY {
295+
// Set terminal to raw mode and handle resizing
296+
fd := os.Stdin.Fd()
297+
if term.IsTerminal(fd) {
298+
oldState, err := term.MakeRaw(fd)
299+
if err != nil {
300+
return fmt.Errorf("failed to set raw terminal: %w", err)
301+
}
302+
defer term.RestoreTerminal(fd, oldState)
291303

292-
// Set initial terminal size
293-
c.resizeContainerTTY(ctx, resp.ID, fd)
304+
// Set initial terminal size
305+
c.resizeContainerTTY(ctx, resp.ID, fd)
294306

295-
// Handle terminal resize signals
296-
go c.monitorTTYSize(ctx, resp.ID, fd)
307+
// Handle terminal resize signals
308+
go c.monitorTTYSize(ctx, resp.ID, fd)
309+
}
297310
}
298311

299312
// Forward SIGINT/SIGTERM to stop the container
@@ -308,55 +321,65 @@ func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
308321
}
309322
}()
310323

311-
// Copy stdin to container, intercepting triple Ctrl-C to kill
312-
// Use a context to stop the goroutine when the container exits
313-
stdinCtx, stdinCancel := context.WithCancel(ctx)
314-
defer stdinCancel()
315-
go func() {
316-
var firstCtrlC time.Time
317-
var ctrlCCount int
318-
buf := make([]byte, 256)
319-
for {
320-
// Check if we should stop
321-
select {
322-
case <-stdinCtx.Done():
323-
return
324-
default:
325-
}
324+
if opts.NoTTY {
325+
if opts.Stdin != nil {
326+
go func() {
327+
_, _ = io.Copy(attachResp.Conn, opts.Stdin)
328+
_ = attachResp.CloseWrite()
329+
}()
330+
}
331+
_, _ = stdcopy.StdCopy(stdout, stderr, attachResp.Reader)
332+
} else {
333+
// Copy stdin to container, intercepting triple Ctrl-C to kill
334+
// Use a context to stop the goroutine when the container exits
335+
stdinCtx, stdinCancel := context.WithCancel(ctx)
336+
defer stdinCancel()
337+
go func() {
338+
var firstCtrlC time.Time
339+
var ctrlCCount int
340+
buf := make([]byte, 256)
341+
for {
342+
// Check if we should stop
343+
select {
344+
case <-stdinCtx.Done():
345+
return
346+
default:
347+
}
326348

327-
n, err := os.Stdin.Read(buf)
328-
if n > 0 {
329-
// Check for Ctrl-C (0x03)
330-
for i := 0; i < n; i++ {
331-
if buf[i] == 0x03 {
332-
now := time.Now()
333-
if ctrlCCount > 0 && now.Sub(firstCtrlC) < time.Second {
334-
ctrlCCount++
335-
if ctrlCCount >= 3 {
336-
// Triple Ctrl-C - kill container
337-
c.cli.ContainerKill(ctx, resp.ID, "SIGKILL")
338-
return
349+
n, err := os.Stdin.Read(buf)
350+
if n > 0 {
351+
// Check for Ctrl-C (0x03)
352+
for i := 0; i < n; i++ {
353+
if buf[i] == 0x03 {
354+
now := time.Now()
355+
if ctrlCCount > 0 && now.Sub(firstCtrlC) < time.Second {
356+
ctrlCCount++
357+
if ctrlCCount >= 3 {
358+
// Triple Ctrl-C - kill container
359+
c.cli.ContainerKill(ctx, resp.ID, "SIGKILL")
360+
return
361+
}
362+
} else {
363+
firstCtrlC = now
364+
ctrlCCount = 1
339365
}
340-
} else {
341-
firstCtrlC = now
342-
ctrlCCount = 1
343366
}
344367
}
368+
attachResp.Conn.Write(buf[:n])
369+
}
370+
if err != nil {
371+
break
345372
}
346-
attachResp.Conn.Write(buf[:n])
347-
}
348-
if err != nil {
349-
break
350373
}
351-
}
352-
attachResp.CloseWrite()
353-
}()
374+
attachResp.CloseWrite()
375+
}()
354376

355-
// Copy container output to stdout
356-
io.Copy(os.Stdout, attachResp.Reader)
377+
// Copy container output to stdout
378+
_, _ = io.Copy(stdout, attachResp.Reader)
357379

358-
// Container output is done, cancel stdin copying
359-
stdinCancel()
380+
// Container output is done, cancel stdin copying
381+
stdinCancel()
382+
}
360383

361384
// Wait for the container to finish
362385
select {

‎backend/docker/docker_test.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func TestRunOptions(t *testing.T) {
3535
MountsRW: []string{"/host/rw:/container/rw"},
3636
Env: []string{"KEY=value"},
3737
Args: []string{"arg1", "arg2"},
38+
NoTTY: true,
3839
}
3940

4041
if opts.Image != "test-image" {
@@ -58,4 +59,7 @@ func TestRunOptions(t *testing.T) {
5859
if len(opts.Args) != 2 {
5960
t.Error("unexpected args")
6061
}
62+
if !opts.NoTTY {
63+
t.Error("unexpected no_tty")
64+
}
6165
}

‎backend/fly/fly.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ func (c *Client) cleanupDeployMachines(ctx context.Context) {
212212

213213
// Run creates a Fly machine, syncs files, and connects interactively.
214214
func (c *Client) Run(ctx context.Context, opts backend.RunOptions) error {
215+
if opts.NoTTY {
216+
return fmt.Errorf("--no-tty is not supported with the fly backend")
217+
}
218+
215219
imageRef := fmt.Sprintf("registry.fly.io/%s:%s", c.app, opts.Image)
216220

217221
// 1. Create machine with sleep infinity (keeps it running for sync + reconnect)

0 commit comments

Comments
 (0)