diff --git a/cli/commands/app_run.go b/cli/commands/app_run.go index 48250a0ae..4996a4924 100644 --- a/cli/commands/app_run.go +++ b/cli/commands/app_run.go @@ -1,13 +1,8 @@ package commands import ( - "io" - "os" - "os/signal" "strings" - "github.com/containerd/console" - "golang.org/x/sys/unix" "miren.dev/runtime/api/exec/exec_v1alpha" "miren.dev/runtime/pkg/rpc/stream" ) @@ -18,60 +13,12 @@ func AppRun(ctx *Context, opts struct { Args []string `rest:"true"` }) error { opt := new(exec_v1alpha.ShellOptions) - if len(opts.Args) > 0 { opt.SetCommand(opts.Args) } - winCh := make(chan os.Signal, 1) - winUpdates := make(chan *exec_v1alpha.WindowSize, 1) - - var ( - in io.Reader - out io.Writer - ) - - if con, err := console.ConsoleFromFile(os.Stdin); err == nil { - in = con - out = con - - if csz, err := con.Size(); err == nil { - ws := new(exec_v1alpha.WindowSize) - ws.SetHeight(int32(csz.Height)) - ws.SetWidth(int32(csz.Width)) - opt.SetWinSize(ws) - } - - defer con.Reset() - con.SetRaw() - - signal.Notify(winCh, unix.SIGWINCH) - defer signal.Stop(winCh) - - go func() { - for { - select { - case <-ctx.Done(): - return - case <-winCh: - csz, err := con.Size() - if err != nil { - ctx.Log.Error("failed to get console size", "error", err) - continue - } - - ws := new(exec_v1alpha.WindowSize) - ws.SetHeight(int32(csz.Height)) - ws.SetWidth(int32(csz.Width)) - - winUpdates <- ws - } - } - }() - } else { - in = os.Stdin - out = os.Stdout - } + in, out, winUpdates, cleanup := setupExecIO(ctx, opt) + defer cleanup() cl, err := ctx.RPCClient("dev.miren.runtime/exec") if err != nil { @@ -80,25 +27,19 @@ func AppRun(ctx *Context, opts struct { sec := exec_v1alpha.NewSandboxExecClient(cl) - input := stream.ServeReader(ctx, in) - output := stream.ServeWriter(ctx, out) - - winUS := stream.ChanReader(winUpdates) - results, err := sec.Exec( ctx, "app", opts.App, strings.Join(opts.Args, " "), opt, - input, output, - winUS, + stream.ServeReader(ctx, in), + stream.ServeWriter(ctx, out), + stream.ChanReader(winUpdates), ) if err != nil { return err } - status := results.Code() - ctx.SetExitCode(int(status)) - + ctx.SetExitCode(int(results.Code())) return nil } diff --git a/cli/commands/exec_io.go b/cli/commands/exec_io.go new file mode 100644 index 000000000..10cf25882 --- /dev/null +++ b/cli/commands/exec_io.go @@ -0,0 +1,78 @@ +package commands + +import ( + "io" + "os" + "os/signal" + + "github.com/containerd/console" + "golang.org/x/sys/unix" + "miren.dev/runtime/api/exec/exec_v1alpha" +) + +// setupExecIO wires stdin/stdout for an interactive exec call. +// +// When both stdin and stdout are TTYs, it puts stdin in raw mode, fills in +// opt.WinSize, and starts a SIGWINCH goroutine that pushes resize events onto +// the returned channel. Otherwise it returns os.Stdin/os.Stdout untouched and +// leaves opt.WinSize unset, which signals the server not to allocate a PTY +// (preserving binary output — see MIR-1001). +// +// The returned cleanup func resets the console and stops the signal handler +// and must always be deferred by the caller. +func setupExecIO(ctx *Context, opt *exec_v1alpha.ShellOptions) ( + io.Reader, + io.Writer, + <-chan *exec_v1alpha.WindowSize, + func(), +) { + winUpdates := make(chan *exec_v1alpha.WindowSize, 1) + + stdinCon, stdinErr := console.ConsoleFromFile(os.Stdin) + stdoutCon, stdoutErr := console.ConsoleFromFile(os.Stdout) + if stdinErr != nil || stdoutErr != nil { + return os.Stdin, os.Stdout, winUpdates, func() {} + } + + if csz, err := stdinCon.Size(); err == nil { + ws := new(exec_v1alpha.WindowSize) + ws.SetHeight(int32(csz.Height)) + ws.SetWidth(int32(csz.Width)) + opt.SetWinSize(ws) + } + + if err := stdinCon.SetRaw(); err != nil { + ctx.Log.Error("failed to set raw mode on stdin", "error", err) + } + + winCh := make(chan os.Signal, 1) + signal.Notify(winCh, unix.SIGWINCH) + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-winCh: + csz, err := stdinCon.Size() + if err != nil { + ctx.Log.Error("failed to get console size", "error", err) + continue + } + + ws := new(exec_v1alpha.WindowSize) + ws.SetHeight(int32(csz.Height)) + ws.SetWidth(int32(csz.Width)) + + winUpdates <- ws + } + } + }() + + cleanup := func() { + signal.Stop(winCh) + stdinCon.Reset() + } + + return stdinCon, stdoutCon, winUpdates, cleanup +} diff --git a/cli/commands/sandbox_exec.go b/cli/commands/sandbox_exec.go index ee8d95bfc..050b3b863 100644 --- a/cli/commands/sandbox_exec.go +++ b/cli/commands/sandbox_exec.go @@ -2,13 +2,8 @@ package commands import ( "fmt" - "io" - "os" - "os/signal" "strings" - "github.com/containerd/console" - "golang.org/x/sys/unix" "miren.dev/runtime/api/exec/exec_v1alpha" "miren.dev/runtime/pkg/rpc/stream" ) @@ -35,72 +30,20 @@ func SandboxExec(ctx *Context, opts struct { sec := exec_v1alpha.NewSandboxExecClient(cl) - winCh := make(chan os.Signal, 1) - winUpdates := make(chan *exec_v1alpha.WindowSize, 1) - opt := new(exec_v1alpha.ShellOptions) - - var ( - in io.Reader - out io.Writer - ) - opt.SetCommand(args) - // Set up interactive console if available, otherwise use standard streams - if con, err := console.ConsoleFromFile(os.Stdin); err == nil { - in = con - out = con - - if csz, err := con.Size(); err == nil { - ws := new(exec_v1alpha.WindowSize) - ws.SetHeight(int32(csz.Height)) - ws.SetWidth(int32(csz.Width)) - opt.SetWinSize(ws) - } - - defer con.Reset() - con.SetRaw() - - signal.Notify(winCh, unix.SIGWINCH) - defer signal.Stop(winCh) - - go func() { - for { - select { - case <-ctx.Done(): - return - case <-winCh: - csz, err := con.Size() - if err != nil { - ctx.Log.Error("failed to get console size", "error", err) - continue - } - - ws := new(exec_v1alpha.WindowSize) - ws.SetHeight(int32(csz.Height)) - ws.SetWidth(int32(csz.Width)) - - winUpdates <- ws - } - } - }() - } else { - in = os.Stdin - out = os.Stdout - } - - input := stream.ServeReader(ctx, in) - output := stream.ServeWriter(ctx, out) - winUS := stream.ChanReader(winUpdates) + in, out, winUpdates, cleanup := setupExecIO(ctx, opt) + defer cleanup() res, err := sec.Exec( ctx, "id", id, strings.Join(args, " "), opt, - input, output, - winUS, + stream.ServeReader(ctx, in), + stream.ServeWriter(ctx, out), + stream.ChanReader(winUpdates), ) if err != nil { return err diff --git a/servers/exec_proxy/exec_proxy.go b/servers/exec_proxy/exec_proxy.go index 2f2a0032a..a49b70ef1 100644 --- a/servers/exec_proxy/exec_proxy.go +++ b/servers/exec_proxy/exec_proxy.go @@ -65,6 +65,7 @@ func (s *Server) Exec(ctx context.Context, req *exec_v1alpha.SandboxExecExec) er } found = ret.Entity().Entity() + id = found.Id().String() case "app": name := args.Value()