diff --git a/TODO.md b/TODO.md index 27fd932f..de61fa4e 100644 --- a/TODO.md +++ b/TODO.md @@ -15,7 +15,7 @@ - [x] `ls` - [x] `new` - [x] `backup` - - [ ] `deploy` (WIP: decentral1se) + - [ ] `deploy` - [x] `check` - [x] `version` - [x] `config` @@ -24,7 +24,7 @@ - [x] `ps` - [x] `restore` - [x] `rm` - - [ ] `run` + - [ ] `run` (WIP: decentral1se) - [ ] `rollback` - [ ] `secret` - [ ] `generate` diff --git a/cli/app/run.go b/cli/app/run.go index 99fc6233..508206e0 100644 --- a/cli/app/run.go +++ b/cli/app/run.go @@ -1,6 +1,16 @@ package app import ( + "context" + "errors" + "fmt" + + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/client" + "coopcloud.tech/abra/config" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -25,4 +35,97 @@ var appRunCommand = &cli.Command{ userFlag, }, ArgsUsage: " ...", + Usage: "run a command in a service container", + Action: func(c *cli.Context) error { + appName := c.Args().First() + if appName == "" { + internal.ShowSubcommandHelpAndError(c, errors.New("no app name provided")) + } + + if c.Args().Len() < 2 { + internal.ShowSubcommandHelpAndError(c, errors.New("no provided")) + } + + appFiles, err := config.LoadAppFiles("") + if err != nil { + logrus.Fatal(err) + } + + host := appFiles[appName].Server + ctx := context.Background() + cl, err := client.NewClientWithContext(host) + if err != nil { + logrus.Fatal(err) + } + + appEnv, err := config.GetApp(appFiles, appName) + if err != nil { + logrus.Fatal(err) + } + + serviceName := c.Args().Get(1) + filters := filters.NewArgs() + filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), serviceName)) + + containers, err := cl.ContainerList(ctx, types.ContainerListOptions{Filters: filters}) + if err != nil { + logrus.Fatal(err) + } + if len(containers) > 1 { + logrus.Fatalf("expected 1 container but got %d", len(containers)) + } + + cmd := c.Args().Slice()[2:] + execCreateOpts := types.ExecConfig{ + //AttachStderr: true, + AttachStdin: true, + //AttachStdout: true, + Cmd: cmd, + Detach: false, + Tty: true, + } + if user != "" { + execCreateOpts.User = user + } + if noTTY { + execCreateOpts.Tty = false + } + + // container := containers[0] + // idResp, err := cl.ContainerExecCreate(ctx, container.ID, execCreateOpts) + // if err != nil { + // logrus.Fatal(err) + // } + + // execAttachOpts := types.ExecStartCheck{Detach: false, Tty: true} + // hResp, err := cl.ContainerExecAttach(ctx, idResp.ID, execAttachOpts) + // if err != nil { + // logrus.Fatal(err) + // } + // defer hResp.Close() + + // var outBuf, errBuf bytes.Buffer + // outputDone := make(chan error) + // go func() { + // _, err = stdcopy.StdCopy(&outBuf, &errBuf, hResp.Reader) + // outputDone <- err + // }() + + // select { + // case err := <-outputDone: + // if err != nil { + // logrus.Fatal(err) + // } + // break + // case <-ctx.Done(): + // break + // } + + // iresp, err := cl.ContainerExecInspect(ctx, idResp.ID) + // if err != nil { + // logrus.Fatal(err) + // } + + return nil + }, } diff --git a/client/container/LICENSE b/client/container/LICENSE new file mode 100644 index 00000000..9c8e20ab --- /dev/null +++ b/client/container/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2013-2017 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/client/container/README.md b/client/container/README.md new file mode 100644 index 00000000..e1472d2d --- /dev/null +++ b/client/container/README.md @@ -0,0 +1,7 @@ +# github.com/docker/cli/cli/command/container + +Due to this literally just being copy-pasted from the lib, the Apache license +will be posted in this folder. Small edits to the source code have been to +function names and parts we don't need deleted. + +Same vibe as [../convert](../convert). diff --git a/client/container/exec.go b/client/container/exec.go new file mode 100644 index 00000000..22db6f8c --- /dev/null +++ b/client/container/exec.go @@ -0,0 +1,187 @@ +package container + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/opts" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cliconfig/configfile" + apiclient "github.com/docker/docker/client" + "github.com/sirupsen/logrus" +) + +type execOptions struct { + detachKeys string + interactive bool + tty bool + detach bool + user string + privileged bool + env opts.ListOpts + workdir string + container string + command []string + envFile opts.ListOpts +} + +func RunExec(dockerCli command.Cli, options execOptions) error { + execConfig, err := parseExec(options, dockerCli.ConfigFile()) + if err != nil { + return err + } + + ctx := context.Background() + client := dockerCli.Client() + + // We need to check the tty _before_ we do the ContainerExecCreate, because + // otherwise if we error out we will leak execIDs on the server (and + // there's no easy way to clean those up). But also in order to make "not + // exist" errors take precedence we do a dummy inspect first. + if _, err := client.ContainerInspect(ctx, options.container); err != nil { + return err + } + if !execConfig.Detach { + if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { + return err + } + } + + response, err := client.ContainerExecCreate(ctx, options.container, *execConfig) + if err != nil { + return err + } + + execID := response.ID + if execID == "" { + return errors.New("exec ID empty") + } + + if execConfig.Detach { + execStartCheck := types.ExecStartCheck{ + Detach: execConfig.Detach, + Tty: execConfig.Tty, + } + return client.ContainerExecStart(ctx, execID, execStartCheck) + } + return interactiveExec(ctx, dockerCli, execConfig, execID) +} + +func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error { + // Interactive exec requested. + var ( + out, stderr io.Writer + in io.ReadCloser + ) + + if execConfig.AttachStdin { + in = dockerCli.In() + } + if execConfig.AttachStdout { + out = dockerCli.Out() + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = dockerCli.Out() + } else { + stderr = dockerCli.Err() + } + } + + client := dockerCli.Client() + execStartCheck := types.ExecStartCheck{ + Tty: execConfig.Tty, + } + resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck) + if err != nil { + return err + } + defer resp.Close() + + errCh := make(chan error, 1) + + go func() { + defer close(errCh) + errCh <- func() error { + streamer := hijackedIOStreamer{ + streams: dockerCli, + inputStream: in, + outputStream: out, + errorStream: stderr, + resp: resp, + tty: execConfig.Tty, + detachKeys: execConfig.DetachKeys, + } + + return streamer.stream(ctx) + }() + }() + + if execConfig.Tty && dockerCli.In().IsTerminal() { + if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { + fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) + } + } + + if err := <-errCh; err != nil { + logrus.Debugf("Error hijack: %s", err) + return err + } + + return getExecExitStatus(ctx, client, execID) +} + +func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error { + resp, err := client.ContainerExecInspect(ctx, execID) + if err != nil { + // If we can't connect, then the daemon probably died. + if !apiclient.IsErrConnectionFailed(err) { + return err + } + return cli.StatusError{StatusCode: -1} + } + status := resp.ExitCode + if status != 0 { + return cli.StatusError{StatusCode: status} + } + return nil +} + +// parseExec parses the specified args for the specified command and generates +// an ExecConfig from it. +func parseExec(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) { + execConfig := &types.ExecConfig{ + User: execOpts.user, + Privileged: execOpts.privileged, + Tty: execOpts.tty, + Cmd: execOpts.command, + Detach: execOpts.detach, + WorkingDir: execOpts.workdir, + } + + // collect all the environment variables for the container + var err error + if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.envFile.GetAll(), execOpts.env.GetAll()); err != nil { + return nil, err + } + + // If -d is not set, attach to everything by default + if !execOpts.detach { + execConfig.AttachStdout = true + execConfig.AttachStderr = true + if execOpts.interactive { + execConfig.AttachStdin = true + } + } + + if execOpts.detachKeys != "" { + execConfig.DetachKeys = execOpts.detachKeys + } else { + execConfig.DetachKeys = configFile.DetachKeys + } + return execConfig, nil +} diff --git a/client/container/hijack.go b/client/container/hijack.go new file mode 100644 index 00000000..433a1c81 --- /dev/null +++ b/client/container/hijack.go @@ -0,0 +1,208 @@ +package container + +import ( + "context" + "fmt" + "io" + "runtime" + "sync" + + "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/term" + "github.com/sirupsen/logrus" +) + +// The default escape key sequence: ctrl-p, ctrl-q +// TODO: This could be moved to `pkg/term`. +var defaultEscapeKeys = []byte{16, 17} + +// A hijackedIOStreamer handles copying input to and output from streams to the +// connection. +type hijackedIOStreamer struct { + streams command.Streams + inputStream io.ReadCloser + outputStream io.Writer + errorStream io.Writer + + resp types.HijackedResponse + + tty bool + detachKeys string +} + +// stream handles setting up the IO and then begins streaming stdin/stdout +// to/from the hijacked connection, blocking until it is either done reading +// output, the user inputs the detach key sequence when in TTY mode, or when +// the given context is cancelled. +func (h *hijackedIOStreamer) stream(ctx context.Context) error { + restoreInput, err := h.setupInput() + if err != nil { + return fmt.Errorf("unable to setup input stream: %s", err) + } + + defer restoreInput() + + outputDone := h.beginOutputStream(restoreInput) + inputDone, detached := h.beginInputStream(restoreInput) + + select { + case err := <-outputDone: + return err + case <-inputDone: + // Input stream has closed. + if h.outputStream != nil || h.errorStream != nil { + // Wait for output to complete streaming. + select { + case err := <-outputDone: + return err + case <-ctx.Done(): + return ctx.Err() + } + } + return nil + case err := <-detached: + // Got a detach key sequence. + return err + case <-ctx.Done(): + return ctx.Err() + } +} + +func (h *hijackedIOStreamer) setupInput() (restore func(), err error) { + if h.inputStream == nil || !h.tty { + // No need to setup input TTY. + // The restore func is a nop. + return func() {}, nil + } + + if err := setRawTerminal(h.streams); err != nil { + return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err) + } + + // Use sync.Once so we may call restore multiple times but ensure we + // only restore the terminal once. + var restoreOnce sync.Once + restore = func() { + restoreOnce.Do(func() { + restoreTerminal(h.streams, h.inputStream) + }) + } + + // Wrap the input to detect detach escape sequence. + // Use default escape keys if an invalid sequence is given. + escapeKeys := defaultEscapeKeys + if h.detachKeys != "" { + customEscapeKeys, err := term.ToBytes(h.detachKeys) + if err != nil { + logrus.Warnf("invalid detach escape keys, using default: %s", err) + } else { + escapeKeys = customEscapeKeys + } + } + + h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close) + + return restore, nil +} + +func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error { + if h.outputStream == nil && h.errorStream == nil { + // There is no need to copy output. + return nil + } + + outputDone := make(chan error) + go func() { + var err error + + // When TTY is ON, use regular copy + if h.outputStream != nil && h.tty { + _, err = io.Copy(h.outputStream, h.resp.Reader) + // We should restore the terminal as soon as possible + // once the connection ends so any following print + // messages will be in normal type. + restoreInput() + } else { + _, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader) + } + + logrus.Debug("[hijack] End of stdout") + + if err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + } + + outputDone <- err + }() + + return outputDone +} + +func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) { + inputDone := make(chan struct{}) + detached := make(chan error) + + go func() { + if h.inputStream != nil { + _, err := io.Copy(h.resp.Conn, h.inputStream) + // We should restore the terminal as soon as possible + // once the connection ends so any following print + // messages will be in normal type. + restoreInput() + + logrus.Debug("[hijack] End of stdin") + + if _, ok := err.(term.EscapeError); ok { + detached <- err + return + } + + if err != nil { + // This error will also occur on the receive + // side (from stdout) where it will be + // propagated back to the caller. + logrus.Debugf("Error sendStdin: %s", err) + } + } + + if err := h.resp.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) + } + + close(inputDone) + }() + + return inputDone, detached +} + +func setRawTerminal(streams command.Streams) error { + if err := streams.In().SetRawTerminal(); err != nil { + return err + } + return streams.Out().SetRawTerminal() +} + +// nolint: unparam +func restoreTerminal(streams command.Streams, in io.Closer) error { + streams.In().RestoreTerminal() + streams.Out().RestoreTerminal() + // WARNING: DO NOT REMOVE THE OS CHECKS !!! + // For some reason this Close call blocks on darwin.. + // As the client exits right after, simply discard the close + // until we find a better solution. + // + // This can also cause the client on Windows to get stuck in Win32 CloseHandle() + // in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442 + // Tracked internally at Microsoft by VSO #11352156. In the + // Windows case, you hit this if you are using the native/v2 console, + // not the "legacy" console, and you start the client in a new window. eg + // `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar` + // will hang. Remove start, and it won't repro. + if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" { + return in.Close() + } + return nil +} diff --git a/client/container/tty.go b/client/container/tty.go new file mode 100644 index 00000000..cc64f999 --- /dev/null +++ b/client/container/tty.go @@ -0,0 +1,97 @@ +package container + +import ( + "context" + "fmt" + "os" + gosignal "os/signal" + "runtime" + "time" + + "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/moby/sys/signal" + "github.com/sirupsen/logrus" +) + +// resizeTtyTo resizes tty to specific height and width +func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width uint, isExec bool) error { + if height == 0 && width == 0 { + return nil + } + + options := types.ResizeOptions{ + Height: height, + Width: width, + } + + var err error + if isExec { + err = client.ContainerExecResize(ctx, id, options) + } else { + err = client.ContainerResize(ctx, id, options) + } + + if err != nil { + logrus.Debugf("Error resize: %s\r", err) + } + return err +} + +// resizeTty is to resize the tty with cli out's tty size +func resizeTty(ctx context.Context, cli command.Cli, id string, isExec bool) error { + height, width := cli.Out().GetTtySize() + return resizeTtyTo(ctx, cli.Client(), id, height, width, isExec) +} + +// initTtySize is to init the tty's size to the same as the window, if there is an error, it will retry 5 times. +func initTtySize(ctx context.Context, cli command.Cli, id string, isExec bool, resizeTtyFunc func(ctx context.Context, cli command.Cli, id string, isExec bool) error) { + rttyFunc := resizeTtyFunc + if rttyFunc == nil { + rttyFunc = resizeTty + } + if err := rttyFunc(ctx, cli, id, isExec); err != nil { + go func() { + var err error + for retry := 0; retry < 5; retry++ { + time.Sleep(10 * time.Millisecond) + if err = rttyFunc(ctx, cli, id, isExec); err == nil { + break + } + } + if err != nil { + fmt.Fprintln(cli.Err(), "failed to resize tty, using default size") + } + }() + } +} + +// MonitorTtySize updates the container tty size when the terminal tty changes size +func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool) error { + initTtySize(ctx, cli, id, isExec, resizeTty) + if runtime.GOOS == "windows" { + go func() { + prevH, prevW := cli.Out().GetTtySize() + for { + time.Sleep(time.Millisecond * 250) + h, w := cli.Out().GetTtySize() + + if prevW != w || prevH != h { + resizeTty(ctx, cli, id, isExec) + } + prevH = h + prevW = w + } + }() + } else { + sigchan := make(chan os.Signal, 1) + gosignal.Notify(sigchan, signal.SIGWINCH) + go func() { + for range sigchan { + resizeTty(ctx, cli, id, isExec) + } + }() + } + return nil +}