From 59f4f8348a85c78199fe6f0106ebe861a2fc5508 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 9 Sep 2022 18:54:54 +0200 Subject: [PATCH] Manual sync of files pressing p (#6089) * Set character mode for input stream * Move watchers in receiver + sync when p pressed * Integration tests manual sync * Add a console to DevSession * Vendor * Skip pressKey tests on Windows * Add interactive test for p press * Add info about pressing p key * Doc * Review * Rephrase Manul sync * Fix HotReloadCapable * Document timers * Document enableCharInput * Document geyKey and getKeyWatcher functions * Avoid to Kill in AfterEach after running Kill before --- .ibm/pipelines/windows-test-script.ps1 | 2 + docs/website/docs/command-reference/dev.md | 20 +- go.mod | 8 +- pkg/watch/file_watcher.go | 137 ++++ pkg/watch/key_watcher.go | 74 ++ pkg/watch/key_watcher_unix.go | 24 + pkg/watch/key_watcher_windows.go | 20 + pkg/watch/term_unix_bsd.go | 13 + pkg/watch/term_unix_other.go | 13 + pkg/watch/watch.go | 215 ++--- pkg/watch/watch_test.go | 11 +- tests/helper/helper_dev.go | 40 +- tests/helper/helper_interactive.go | 4 + tests/integration/cmd_add_binding_test.go | 7 + tests/integration/cmd_delete_test.go | 2 +- tests/integration/cmd_dev_debug_test.go | 4 - tests/integration/cmd_dev_test.go | 472 ++++++----- tests/integration/interactive_dev_test.go | 30 +- vendor/github.com/hinshun/vt10x/.travis.yml | 5 - vendor/github.com/hinshun/vt10x/LICENSE | 19 - vendor/github.com/hinshun/vt10x/README.md | 9 - vendor/github.com/hinshun/vt10x/color.go | 38 - vendor/github.com/hinshun/vt10x/csi.go | 189 ----- vendor/github.com/hinshun/vt10x/doc.go | 9 - .../github.com/hinshun/vt10x/ioctl_other.go | 15 - .../github.com/hinshun/vt10x/ioctl_posix.go | 31 - vendor/github.com/hinshun/vt10x/parse.go | 203 ----- vendor/github.com/hinshun/vt10x/state.go | 762 ------------------ vendor/github.com/hinshun/vt10x/str.go | 330 -------- vendor/github.com/hinshun/vt10x/vt.go | 89 -- vendor/github.com/hinshun/vt10x/vt_other.go | 107 --- vendor/github.com/hinshun/vt10x/vt_posix.go | 108 --- vendor/modules.txt | 1 - 33 files changed, 726 insertions(+), 2285 deletions(-) create mode 100644 pkg/watch/file_watcher.go create mode 100644 pkg/watch/key_watcher.go create mode 100644 pkg/watch/key_watcher_unix.go create mode 100644 pkg/watch/key_watcher_windows.go create mode 100644 pkg/watch/term_unix_bsd.go create mode 100644 pkg/watch/term_unix_other.go delete mode 100644 vendor/github.com/hinshun/vt10x/.travis.yml delete mode 100644 vendor/github.com/hinshun/vt10x/LICENSE delete mode 100644 vendor/github.com/hinshun/vt10x/README.md delete mode 100644 vendor/github.com/hinshun/vt10x/color.go delete mode 100644 vendor/github.com/hinshun/vt10x/csi.go delete mode 100644 vendor/github.com/hinshun/vt10x/doc.go delete mode 100644 vendor/github.com/hinshun/vt10x/ioctl_other.go delete mode 100644 vendor/github.com/hinshun/vt10x/ioctl_posix.go delete mode 100644 vendor/github.com/hinshun/vt10x/parse.go delete mode 100644 vendor/github.com/hinshun/vt10x/state.go delete mode 100644 vendor/github.com/hinshun/vt10x/str.go delete mode 100644 vendor/github.com/hinshun/vt10x/vt.go delete mode 100644 vendor/github.com/hinshun/vt10x/vt_other.go delete mode 100644 vendor/github.com/hinshun/vt10x/vt_posix.go diff --git a/.ibm/pipelines/windows-test-script.ps1 b/.ibm/pipelines/windows-test-script.ps1 index 5a0773cdb..99d2051a5 100644 --- a/.ibm/pipelines/windows-test-script.ps1 +++ b/.ibm/pipelines/windows-test-script.ps1 @@ -58,6 +58,8 @@ function Run-Test { [Environment]::SetEnvironmentVariable("TEST_EXEC_NODES", "$TEST_EXEC_NODES") [Environment]::SetEnvironmentVariable("SKIP_USER_LOGIN_TESTS","true") [Environment]::SetEnvironmentVariable("SKIP_WELCOMING_MESSAGES","true") + # Integration tests detecting key press when running DevSession are not working on Windows + [Environment]::SetEnvironmentVariable("SKIP_KEY_PRESS","true") Shout "Login IBMcloud" ibmcloud login --apikey ${API_KEY} diff --git a/docs/website/docs/command-reference/dev.md b/docs/website/docs/command-reference/dev.md index 05b4b7166..19894a3c6 100644 --- a/docs/website/docs/command-reference/dev.md +++ b/docs/website/docs/command-reference/dev.md @@ -35,7 +35,9 @@ Your application is now running on the cluster - Forwarding from 127.0.0.1:40001 -> 3000 Watching for changes in the current directory /Users/user/nodejs -Press Ctrl+c to exit `odo dev` and delete resources from the cluster + +[Ctrl+c] - Exit and delete resources from the cluster + [p] - Manually apply local changes to the application on the cluster ``` In the above example, three things have happened: @@ -46,6 +48,22 @@ In the above example, three things have happened: You can press Ctrl-c at any time to terminate the development session. The command can take a few moment to terminate, as it will first delete all resources deployed into the cluster for this session before terminating. +### Applying local changes to the application on the cluster + +By default, the changes made by the user to the Devfile and source files are applied directly. + +The flag `--no-watch` can be used to change this behaviour: when the user changes the devfile or any source file, the changes +won't be applied immediately, but the next time the user presses the `p` key. + +Depending on the local changes, different events can occur on the cluster: + +- if source files are modified, they are pushed to the container running the application, and: + - if the `run` command is marked as `HotReloadCapable`, the application is responsible for applying the new changes + - if the `run` command is not marked as `HotReloadCapable`, the application is stopped, then restarted by odo using the `build` and `run` commands again. +- if the Devfile is modified, the deployment of the application is modified with the new changes. In some circumstances, this may + cause the restart of the container running the application and therefore the application itself. + + ### Running an alternative command #### Running an alternative build command diff --git a/go.mod b/go.mod index 46d929eb2..10cdc2cdc 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.17 require ( github.com/ActiveState/termtest v0.7.1 + github.com/ActiveState/termtest/expect v0.7.0 github.com/AlecAivazis/survey/v2 v2.3.5 - github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/Xuanwo/go-locale v1.1.0 github.com/blang/semver v3.5.1+incompatible github.com/devfile/api/v2 v2.0.0-20220309195345-48ebbf1e51cf @@ -19,9 +19,7 @@ require ( github.com/go-git/go-git/v5 v5.3.0 github.com/go-openapi/spec v0.19.5 github.com/golang/mock v1.6.0 - github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 github.com/jedib0t/go-pretty/v6 v6.3.5 - github.com/kr/pty v1.1.8 github.com/kubernetes-sigs/service-catalog v0.3.1 github.com/kylelemons/godebug v1.1.0 github.com/mattn/go-colorable v0.1.9 @@ -68,7 +66,6 @@ require ( require ( cloud.google.com/go v0.81.0 // indirect github.com/ActiveState/termtest/conpty v0.5.0 // indirect - github.com/ActiveState/termtest/expect v0.7.0 // indirect github.com/ActiveState/termtest/xpty v0.6.0 // indirect github.com/ActiveState/vt10x v1.3.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -81,6 +78,7 @@ require ( github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect github.com/Microsoft/go-winio v0.5.0 // indirect github.com/Microsoft/hcsshim v0.8.20 // indirect + github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2 // indirect @@ -129,6 +127,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-version v1.4.0 // indirect + github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -138,6 +137,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/klauspost/compress v1.11.13 // indirect + github.com/kr/pty v1.1.8 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/pkg/watch/file_watcher.go b/pkg/watch/file_watcher.go new file mode 100644 index 000000000..00cd03d9f --- /dev/null +++ b/pkg/watch/file_watcher.go @@ -0,0 +1,137 @@ +package watch + +import ( + "fmt" + "os" + "path/filepath" + + dfutil "github.com/devfile/library/pkg/util" + "github.com/fsnotify/fsnotify" + "github.com/redhat-developer/odo/pkg/util" + gitignore "github.com/sabhiram/go-gitignore" + "k8s.io/klog" +) + +func getFullSourcesWatcher(path string, fileIgnores []string) (*fsnotify.Watcher, error) { + absIgnorePaths := dfutil.GetAbsGlobExps(path, fileIgnores) + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, fmt.Errorf("error setting up filesystem watcher: %v", err) + } + + // adding watch on the root folder and the sub folders recursively + // so directory and the path in addRecursiveWatch() are the same + err = addRecursiveWatch(watcher, path, path, absIgnorePaths) + if err != nil { + return nil, fmt.Errorf("error watching source path %s: %v", path, err) + } + return watcher, nil +} + +// addRecursiveWatch handles adding watches recursively for the path provided +// and its subdirectories. If a non-directory is specified, this call is a no-op. +// Files matching glob pattern defined in ignores will be ignored. +// Taken from https://github.com/openshift/origin/blob/85eb37b34f0657631592356d020cef5a58470f8e/pkg/util/fsnotification/fsnotification.go +// rootPath is the root path of the file or directory, +// path is the recursive path of the file or the directory, +// ignores contains the glob rules for matching +func addRecursiveWatch(watcher *fsnotify.Watcher, rootPath string, path string, ignores []string) error { + + file, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("error introspecting path %s: %v", path, err) + } + + ignoreMatcher := gitignore.CompileIgnoreLines(ignores...) + + mode := file.Mode() + if mode.IsRegular() { + var rel string + rel, err = filepath.Rel(rootPath, path) + if err != nil { + return err + } + matched := ignoreMatcher.MatchesPath(rel) + if !matched { + klog.V(4).Infof("adding watch on path %s", path) + + // checking if the file exits before adding the watcher to it + if !util.CheckPathExists(path) { + return nil + } + + err = watcher.Add(path) + if err != nil { + klog.V(4).Infof("error adding watcher for path %s: %v", path, err) + } + return nil + } + } + + folders := []string{} + err = filepath.Walk(path, func(newPath string, info os.FileInfo, err error) error { + if err != nil { + // Ignore the error if it's a 'path does not exist' error, no need to walk a non-existent path + if !util.CheckPathExists(newPath) { + klog.V(4).Infof("Walk func received an error for path %s, but the path doesn't exist so this is likely not an error. err: %v", path, err) + return nil + } + return fmt.Errorf("unable to walk path: %s: %w", newPath, err) + } + + if info.IsDir() { + // If the current directory matches any of the ignore patterns, ignore them so that their contents are also not ignored + rel, err := filepath.Rel(rootPath, newPath) + if err != nil { + return err + } + matched := ignoreMatcher.MatchesPath(rel) + if err != nil { + return fmt.Errorf("unable to addRecursiveWatch on %s: %w", newPath, err) + } + if matched { + klog.V(4).Infof("ignoring watch on path %s", newPath) + return filepath.SkipDir + } + // Append the folder we just walked on + folders = append(folders, newPath) + } + return nil + }) + if err != nil { + return err + } + for _, folder := range folders { + + rel, err := filepath.Rel(rootPath, folder) + if err != nil { + return err + } + matched := ignoreMatcher.MatchesPath(rel) + + if matched { + klog.V(4).Infof("ignoring watch for %s", folder) + continue + } + + // checking if the file exits before adding the watcher to it + if !util.CheckPathExists(path) { + continue + } + + klog.V(4).Infof("adding watch on path %s", folder) + err = watcher.Add(folder) + if err != nil { + // Linux "no space left on device" issues are usually resolved via + // $ sudo sysctl fs.inotify.max_user_watches=65536 + // BSD / OSX: "too many open files" issues are ussualy resolved via + // $ sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", + klog.V(4).Infof("error adding watcher for path %s: %v", folder, err) + } + } + return nil +} diff --git a/pkg/watch/key_watcher.go b/pkg/watch/key_watcher.go new file mode 100644 index 000000000..3af24fd7e --- /dev/null +++ b/pkg/watch/key_watcher.go @@ -0,0 +1,74 @@ +package watch + +import ( + "context" + "fmt" + "io" + "os" + + "golang.org/x/term" +) + +// getKeyWatcher returns a channel which will emit +// characters when keys are pressed on the keyboard +func getKeyWatcher(ctx context.Context, out io.Writer) <-chan byte { + + keyInput := make(chan byte) + + go func() { + stdinfd := int(os.Stdin.Fd()) + if !term.IsTerminal(stdinfd) { + return + } + + // First set the terminal in character mode + // to be able to read characters as soon as + // they are emitted, instead of waiting + // for newline characters + oldState, err := term.GetState(stdinfd) + if err != nil { + fmt.Fprintln(out, fmt.Errorf("getstate: %w", err)) + return + } + err = enableCharInput(stdinfd) + if err != nil { + fmt.Fprintln(out, fmt.Errorf("enableCharInput: %w", err)) + return + } + defer func() { + _ = term.Restore(stdinfd, oldState) + }() + + // Wait for the context to be cancelled + // or a character to be emitted + for { + select { + case <-ctx.Done(): + return + case b := <-getKey(out): + keyInput <- b + } + } + }() + + return keyInput +} + +// getKey returns a channel which will emit a character +// when a key is pressed on the keyboard +func getKey(out io.Writer) <-chan byte { + + ch := make(chan byte) + + go func() { + b := make([]byte, 1) + _, err := os.Stdin.Read(b) + if err != nil { + fmt.Fprintln(out, fmt.Errorf("read: %w", err)) + return + } + ch <- b[0] + }() + + return ch +} diff --git a/pkg/watch/key_watcher_unix.go b/pkg/watch/key_watcher_unix.go new file mode 100644 index 000000000..c3d83f0f7 --- /dev/null +++ b/pkg/watch/key_watcher_unix.go @@ -0,0 +1,24 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos + +package watch + +import ( + "golang.org/x/sys/unix" +) + +// enableCharInput is inspired from the Unix implementation of MakeRaw in golang.org/x/term +// It enables the treatment of input stream char by char instead of line by line +// See https://man7.org/linux/man-pages/man3/termios.3.html for reference +func enableCharInput(fd int) error { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return err + } + termios.Lflag &^= unix.ICANON + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { + return err + } + + return nil +} diff --git a/pkg/watch/key_watcher_windows.go b/pkg/watch/key_watcher_windows.go new file mode 100644 index 000000000..5e1314fc2 --- /dev/null +++ b/pkg/watch/key_watcher_windows.go @@ -0,0 +1,20 @@ +package watch + +import ( + "golang.org/x/sys/windows" +) + +// enableCharInput is inspired from Windows implementation of MakeRaw in golang.org/x/term +// It enables the treatment of input stream char by char instead of line by line +// See https://docs.microsoft.com/en-us/windows/console/setconsolemode for reference +func enableCharInput(fd int) error { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return err + } + raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { + return err + } + return nil +} diff --git a/pkg/watch/term_unix_bsd.go b/pkg/watch/term_unix_bsd.go new file mode 100644 index 000000000..89dd35277 --- /dev/null +++ b/pkg/watch/term_unix_bsd.go @@ -0,0 +1,13 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin || dragonfly || freebsd || netbsd || openbsd +// +build darwin dragonfly freebsd netbsd openbsd + +package watch + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TIOCGETA +const ioctlWriteTermios = unix.TIOCSETA diff --git a/pkg/watch/term_unix_other.go b/pkg/watch/term_unix_other.go new file mode 100644 index 000000000..eca916eb5 --- /dev/null +++ b/pkg/watch/term_unix_other.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build aix || linux || solaris || zos +// +build aix linux solaris zos + +package watch + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index 139213797..4b45ee116 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -24,9 +24,6 @@ import ( gitignore "github.com/sabhiram/go-gitignore" "github.com/redhat-developer/odo/pkg/envinfo" - "github.com/redhat-developer/odo/pkg/util" - - dfutil "github.com/devfile/library/pkg/util" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -38,13 +35,26 @@ import ( const ( // PushErrorString is the string that is printed when an error occurs during watch's Push operation PushErrorString = "Error occurred on Push" - CtrlCMessage = "Press Ctrl+c to exit `odo dev` and delete resources from the cluster" + PromptMessage = ` +[Ctrl+c] - Exit and delete resources from the cluster + [p] - Manually apply local changes to the application on the cluster +` ) type WatchClient struct { kubeClient kclient.ClientInterface deleteClient _delete.Client stateClient state.Client + + sourcesWatcher *fsnotify.Watcher + deploymentWatcher watch.Interface + devfileWatcher *fsnotify.Watcher + podWatcher watch.Interface + warningsWatcher watch.Interface + keyWatcher <-chan byte + + // true to force sync, used when manual sync + forceSync bool } var _ Client = (*WatchClient)(nil) @@ -108,138 +118,30 @@ type evaluateChangesFunc func(events []fsnotify.Event, path string, fileIgnores // It returns a Duration after which to recall in case of error type processEventsFunc func(changedFiles, deletedPaths []string, parameters WatchParameters, out io.Writer, componentStatus *ComponentStatus, backoff *ExpBackoff) (*time.Duration, error) -// addRecursiveWatch handles adding watches recursively for the path provided -// and its subdirectories. If a non-directory is specified, this call is a no-op. -// Files matching glob pattern defined in ignores will be ignored. -// Taken from https://github.com/openshift/origin/blob/85eb37b34f0657631592356d020cef5a58470f8e/pkg/util/fsnotification/fsnotification.go -// rootPath is the root path of the file or directory, -// path is the recursive path of the file or the directory, -// ignores contains the glob rules for matching -func addRecursiveWatch(watcher *fsnotify.Watcher, rootPath string, path string, ignores []string) error { - - file, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("error introspecting path %s: %v", path, err) - } - - ignoreMatcher := gitignore.CompileIgnoreLines(ignores...) - - mode := file.Mode() - if mode.IsRegular() { - var rel string - rel, err = filepath.Rel(rootPath, path) - if err != nil { - return err - } - matched := ignoreMatcher.MatchesPath(rel) - if !matched { - klog.V(4).Infof("adding watch on path %s", path) - - // checking if the file exits before adding the watcher to it - if !util.CheckPathExists(path) { - return nil - } - - err = watcher.Add(path) - if err != nil { - klog.V(4).Infof("error adding watcher for path %s: %v", path, err) - } - return nil - } - } - - folders := []string{} - err = filepath.Walk(path, func(newPath string, info os.FileInfo, err error) error { - if err != nil { - // Ignore the error if it's a 'path does not exist' error, no need to walk a non-existent path - if !util.CheckPathExists(newPath) { - klog.V(4).Infof("Walk func received an error for path %s, but the path doesn't exist so this is likely not an error. err: %v", path, err) - return nil - } - return fmt.Errorf("unable to walk path: %s: %w", newPath, err) - } - - if info.IsDir() { - // If the current directory matches any of the ignore patterns, ignore them so that their contents are also not ignored - rel, err := filepath.Rel(rootPath, newPath) - if err != nil { - return err - } - matched := ignoreMatcher.MatchesPath(rel) - if err != nil { - return fmt.Errorf("unable to addRecursiveWatch on %s: %w", newPath, err) - } - if matched { - klog.V(4).Infof("ignoring watch on path %s", newPath) - return filepath.SkipDir - } - // Append the folder we just walked on - folders = append(folders, newPath) - } - return nil - }) - if err != nil { - return err - } - for _, folder := range folders { - - rel, err := filepath.Rel(rootPath, folder) - if err != nil { - return err - } - matched := ignoreMatcher.MatchesPath(rel) - - if matched { - klog.V(4).Infof("ignoring watch for %s", folder) - continue - } - - // checking if the file exits before adding the watcher to it - if !util.CheckPathExists(path) { - continue - } - - klog.V(4).Infof("adding watch on path %s", folder) - err = watcher.Add(folder) - if err != nil { - // Linux "no space left on device" issues are usually resolved via - // $ sudo sysctl fs.inotify.max_user_watches=65536 - // BSD / OSX: "too many open files" issues are ussualy resolved via - // $ sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", - klog.V(4).Infof("error adding watcher for path %s: %v", folder, err) - } - } - return nil -} - func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context, componentStatus ComponentStatus) error { klog.V(4).Infof("starting WatchAndPush, path: %s, component: %s, ignores %s", parameters.Path, parameters.ComponentName, parameters.FileIgnores) - var sourcesWatcher *fsnotify.Watcher var err error if parameters.WatchFiles { - sourcesWatcher, err = getFullSourcesWatcher(parameters.Path, parameters.FileIgnores) + o.sourcesWatcher, err = getFullSourcesWatcher(parameters.Path, parameters.FileIgnores) if err != nil { return err } } else { - sourcesWatcher, err = fsnotify.NewWatcher() + o.sourcesWatcher, err = fsnotify.NewWatcher() if err != nil { return err } } - defer sourcesWatcher.Close() + defer o.sourcesWatcher.Close() selector := labels.GetSelector(parameters.ComponentName, parameters.ApplicationName, labels.ComponentDevMode, true) - deploymentWatcher, err := o.kubeClient.DeploymentWatcher(ctx, selector) + o.deploymentWatcher, err = o.kubeClient.DeploymentWatcher(ctx, selector) if err != nil { return fmt.Errorf("error watching deployment: %v", err) } - devfileWatcher, err := fsnotify.NewWatcher() + o.devfileWatcher, err = fsnotify.NewWatcher() if err != nil { return err } @@ -251,19 +153,20 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct } devfileFiles = append(devfileFiles, parameters.DevfilePath) for _, f := range devfileFiles { - err = devfileWatcher.Add(f) + err = o.devfileWatcher.Add(f) if err != nil { klog.V(4).Infof("error adding watcher for path %s: %v", f, err) } } } - podWatcher, err := o.kubeClient.PodWatcher(ctx, selector) + o.podWatcher, err = o.kubeClient.PodWatcher(ctx, selector) if err != nil { return err } - warningsWatcher, isForbidden, err := o.kubeClient.PodWarningEventWatcher(ctx) + var isForbidden bool + o.warningsWatcher, isForbidden, err = o.kubeClient.PodWarningEventWatcher(ctx) if err != nil { return err } @@ -271,30 +174,21 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct log.Fwarning(out, "Unable to watch Events resource, warning Events won't be displayed") } - return o.eventWatcher(ctx, sourcesWatcher, deploymentWatcher, devfileWatcher, podWatcher, warningsWatcher, parameters, out, evaluateFileChanges, processEvents, componentStatus) -} - -func getFullSourcesWatcher(path string, fileIgnores []string) (*fsnotify.Watcher, error) { - absIgnorePaths := dfutil.GetAbsGlobExps(path, fileIgnores) - - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, fmt.Errorf("error setting up filesystem watcher: %v", err) - } - - // adding watch on the root folder and the sub folders recursively - // so directory and the path in addRecursiveWatch() are the same - err = addRecursiveWatch(watcher, path, path, absIgnorePaths) - if err != nil { - return nil, fmt.Errorf("error watching source path %s: %v", path, err) - } - return watcher, nil + o.keyWatcher = getKeyWatcher(ctx, out) + return o.eventWatcher(ctx, parameters, out, evaluateFileChanges, processEvents, componentStatus) } // eventWatcher loops till the context's Done channel indicates it to stop looping, at which point it performs cleanup. // While looping, it listens for filesystem events and processes these events using the WatchParameters to push to the remote pod. // It outputs any logs to the out io Writer -func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify.Watcher, deploymentWatcher watch.Interface, devfileWatcher *fsnotify.Watcher, podWatcher watch.Interface, eventsWatcher watch.Interface, parameters WatchParameters, out io.Writer, evaluateChangesHandler evaluateChangesFunc, processEventsHandler processEventsFunc, componentStatus ComponentStatus) error { +func (o *WatchClient) eventWatcher( + ctx context.Context, + parameters WatchParameters, + out io.Writer, + evaluateChangesHandler evaluateChangesFunc, + processEventsHandler processEventsFunc, + componentStatus ComponentStatus, +) error { expBackoff := NewExpBackoff() @@ -309,12 +203,15 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify sourcesTimer := time.NewTimer(time.Millisecond) <-sourcesTimer.C + // devfileTimer has the same usage as sourcesTimer, for file events coming from devfileWatcher devfileTimer := time.NewTimer(time.Millisecond) <-devfileTimer.C + // deployTimer has the same usage as sourcesTimer, for events coming from watching Deployments, from deploymentWatcher deployTimer := time.NewTimer(time.Millisecond) <-deployTimer.C + // retryTimer is a timer used to retry later a sync that has failed retryTimer := time.NewTimer(time.Millisecond) <-retryTimer.C @@ -322,24 +219,31 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify for { select { - case event := <-sourcesWatcher.Events: + case event := <-o.sourcesWatcher.Events: events = append(events, event) // We are waiting for more events in this interval sourcesTimer.Reset(100 * time.Millisecond) + case <-sourcesTimer.C: // timer has fired if !componentCanSyncFile(componentStatus.State) { continue } - // first find the files that have changed (also includes the ones newly created) or deleted - changedFiles, deletedPaths := evaluateChangesHandler(events, parameters.Path, parameters.FileIgnores, sourcesWatcher) - // process the changes and sync files with remote pod - if len(changedFiles) == 0 && len(deletedPaths) == 0 { - continue + + var changedFiles, deletedPaths []string + if !o.forceSync { + // first find the files that have changed (also includes the ones newly created) or deleted + changedFiles, deletedPaths = evaluateChangesHandler(events, parameters.Path, parameters.FileIgnores, o.sourcesWatcher) + // process the changes and sync files with remote pod + if len(changedFiles) == 0 && len(deletedPaths) == 0 { + continue + } } + componentStatus.State = StateSyncOutdated fmt.Fprintf(out, "Pushing files...\n\n") retry, err := processEventsHandler(changedFiles, deletedPaths, parameters, out, &componentStatus, expBackoff) + o.forceSync = false if err != nil { return err } @@ -355,10 +259,16 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify <-retryTimer.C } - case watchErr := <-sourcesWatcher.Errors: + case watchErr := <-o.sourcesWatcher.Errors: return watchErr - case ev := <-deploymentWatcher.ResultChan(): + case key := <-o.keyWatcher: + if key == 'p' { + o.forceSync = true + sourcesTimer.Reset(100 * time.Millisecond) + } + + case ev := <-o.deploymentWatcher.ResultChan(): switch obj := ev.Object.(type) { case *appsv1.Deployment: klog.V(4).Infof("deployment watcher Event: Type: %s, name: %s, rv: %s, pods: %d\n", @@ -381,7 +291,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify <-retryTimer.C } - case <-devfileWatcher.Events: + case <-o.devfileWatcher.Events: devfileTimer.Reset(100 * time.Millisecond) case <-devfileTimer.C: @@ -409,7 +319,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify <-retryTimer.C } - case ev := <-podWatcher.ResultChan(): + case ev := <-o.podWatcher.ResultChan(): switch ev.Type { case watch.Deleted: pod, ok := ev.Object.(*corev1.Pod) @@ -425,7 +335,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify podsPhases.Add(out, pod.GetCreationTimestamp(), pod) } - case ev := <-eventsWatcher.ResultChan(): + case ev := <-o.warningsWatcher.ResultChan(): switch kevent := ev.Object.(type) { case *corev1.Event: podName := kevent.InvolvedObject.Name @@ -439,7 +349,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify } } - case watchErr := <-devfileWatcher.Errors: + case watchErr := <-o.devfileWatcher.Errors: return watchErr case <-ctx.Done(): @@ -635,7 +545,8 @@ func removeDuplicates(input []string) []string { } func printInfoMessage(out io.Writer, path string) { - log.Finfof(out, "\nWatching for changes in the current directory %s\n"+CtrlCMessage+"\n", path) + log.Sectionf("Dev mode") + fmt.Fprintf(out, " %s\n Watching for changes in the current directory %s\n\n %s%s", log.Sbold("Status:"), path, log.Sbold("Keyboard Commands:"), PromptMessage) } func isFatal(err error) bool { diff --git a/pkg/watch/watch_test.go b/pkg/watch/watch_test.go index e9cf8c01f..44ba34a2e 100644 --- a/pkg/watch/watch_test.go +++ b/pkg/watch/watch_test.go @@ -131,8 +131,15 @@ func Test_eventWatcher(t *testing.T) { State: StateReady, } - o := WatchClient{} - err := o.eventWatcher(ctx, watcher, fakeWatcher{}, fileWatcher, fakeWatcher{}, fakeWatcher{}, tt.args.parameters, out, evaluateChangesHandler, processEventsHandler, componentStatus) + o := WatchClient{ + sourcesWatcher: watcher, + deploymentWatcher: fakeWatcher{}, + podWatcher: fakeWatcher{}, + warningsWatcher: fakeWatcher{}, + devfileWatcher: fileWatcher, + keyWatcher: make(chan byte), + } + err := o.eventWatcher(ctx, tt.args.parameters, out, evaluateChangesHandler, processEventsHandler, componentStatus) if (err != nil) != tt.wantErr { t.Errorf("eventWatcher() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/tests/helper/helper_dev.go b/tests/helper/helper_dev.go index c441420a3..00d009b38 100644 --- a/tests/helper/helper_dev.go +++ b/tests/helper/helper_dev.go @@ -1,9 +1,11 @@ package helper import ( + "os" "regexp" "time" + "github.com/ActiveState/termtest/expect" "github.com/onsi/gomega" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -109,6 +111,7 @@ import ( type DevSession struct { session *gexec.Session stopped bool + console *expect.Console } // StartDevMode starts a dev session with `odo dev` @@ -116,16 +119,28 @@ type DevSession struct { // and the redirections endpoints to access ports opened by component // when the dev mode is completely started func StartDevMode(envvars []string, opts ...string) (DevSession, []byte, []byte, map[string]string, error) { + + c, err := expect.NewConsole(expect.WithStdout(os.Stdout)) + if err != nil { + return DevSession{}, nil, nil, nil, err + } + args := []string{"dev", "--random-ports"} args = append(args, opts...) - session := Cmd("odo", args...).AddEnv(envvars...).Runner().session - WaitForOutputToContain("Press Ctrl+c to exit `odo dev` and delete resources from the cluster", 360, 10, session) + cmd := Cmd("odo", args...) + cmd.Cmd.Stdin = c.Tty() + cmd.Cmd.Stdout = c.Tty() + cmd.Cmd.Stderr = c.Tty() + + session := cmd.AddEnv(envvars...).Runner().session + WaitForOutputToContain("[Ctrl+c] - Exit", 360, 10, session) result := DevSession{ session: session, + console: c, } outContents := session.Out.Contents() errContents := session.Err.Contents() - err := session.Out.Clear() + err = session.Out.Clear() if err != nil { return DevSession{}, nil, nil, nil, err } @@ -139,11 +154,19 @@ func StartDevMode(envvars []string, opts ...string) (DevSession, []byte, []byte, // Kill a Dev session abruptly, without handling any cleanup func (o DevSession) Kill() { + if o.console != nil { + err := o.console.Close() + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + } o.session.Kill() } // Stop a Dev session cleanly (equivalent as hitting Ctrl-c) func (o *DevSession) Stop() { + if o.console != nil { + err := o.console.Close() + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + } if o.stopped { return } @@ -152,6 +175,14 @@ func (o *DevSession) Stop() { o.stopped = true } +func (o *DevSession) PressKey(p byte) { + if o.console == nil { + return + } + _, err := o.console.Write([]byte{p}) + Expect(err).ToNot(HaveOccurred()) +} + func (o DevSession) WaitEnd() { o.session.Wait(3 * time.Minute) } @@ -240,7 +271,8 @@ func DevModeShouldFail(envvars []string, substring string, opts ...string) (DevS } // getPorts returns a map of ports redirected depending on the information in s -// `- Forwarding from 127.0.0.1:40001 -> 3000` will return { "3000": "127.0.0.1:40001" } +// +// `- Forwarding from 127.0.0.1:40001 -> 3000` will return { "3000": "127.0.0.1:40001" } func getPorts(s string) map[string]string { result := map[string]string{} re := regexp.MustCompile("(127.0.0.1:[0-9]+) -> ([0-9]+)") diff --git a/tests/helper/helper_interactive.go b/tests/helper/helper_interactive.go index b5d2b4e01..da3110c3b 100644 --- a/tests/helper/helper_interactive.go +++ b/tests/helper/helper_interactive.go @@ -101,6 +101,10 @@ func SendLine(ctx InteractiveContext, line string) { ctx.cp.Send(line) } +func PressKey(ctx InteractiveContext, c byte) { + ctx.cp.SendUnterminated(string(c)) +} + func ExpectString(ctx InteractiveContext, line string) { res, err := ctx.cp.Expect(line, 120*time.Second) fmt.Fprint(ctx.buffer, res) diff --git a/tests/integration/cmd_add_binding_test.go b/tests/integration/cmd_add_binding_test.go index 3877ae656..2d1201290 100644 --- a/tests/integration/cmd_add_binding_test.go +++ b/tests/integration/cmd_add_binding_test.go @@ -254,6 +254,13 @@ status: stdout := commonVar.CliRunner.Run("get", "servicebinding", bindingName).Out.Contents() Expect(stdout).To(ContainSubstring("ApplicationsBound")) }) + }) + + When("odo dev is run", func() { + BeforeEach(func() { + devSession, _, _, _, err = helper.StartDevMode(nil) + Expect(err).ToNot(HaveOccurred()) + }) When("odo dev command is stopped", func() { BeforeEach(func() { diff --git a/tests/integration/cmd_delete_test.go b/tests/integration/cmd_delete_test.go index a1c515c3b..17a85870b 100644 --- a/tests/integration/cmd_delete_test.go +++ b/tests/integration/cmd_delete_test.go @@ -261,7 +261,7 @@ var _ = Describe("odo delete command tests", func() { helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-valid-events.yaml")).ShouldPass() session := helper.CmdRunner("odo", "dev", "--random-ports") defer session.Kill() - helper.WaitForOutputToContain("Press Ctrl+c to exit", 180, 10, session) + helper.WaitForOutputToContain("[Ctrl+c] - Exit", 180, 10, session) // Ensure that the pod is in running state Eventually(string(commonVar.CliRunner.Run("get", "pods", "-n", commonVar.Project).Out.Contents()), 60, 3).Should(ContainSubstring(cmpName)) // running in verbosity since the preStop events information is only printed in v4 diff --git a/tests/integration/cmd_dev_debug_test.go b/tests/integration/cmd_dev_debug_test.go index 12154a067..81bc5b105 100644 --- a/tests/integration/cmd_dev_debug_test.go +++ b/tests/integration/cmd_dev_debug_test.go @@ -171,10 +171,6 @@ var _ = Describe("odo dev debug command tests", func() { session, sessionOut, _, ports, err = helper.StartDevMode([]string{"PODMAN_CMD=echo"}, "--debug") Expect(err).ToNot(HaveOccurred()) }) - AfterEach(func() { - session.Stop() - session.WaitEnd() - }) It("should execute the composite apply commands successfully", func() { checkDeploymentExists := func() { out := commonVar.CliRunner.Run("get", "deployments", deploymentName).Out.Contents() diff --git a/tests/integration/cmd_dev_test.go b/tests/integration/cmd_dev_test.go index 2716d738a..9c31c5ea4 100644 --- a/tests/integration/cmd_dev_test.go +++ b/tests/integration/cmd_dev_test.go @@ -255,11 +255,6 @@ ComponentSettings: Expect(err).ToNot(HaveOccurred()) }) - AfterEach(func() { - devSession.Kill() - devSession.WaitEnd() - }) - When("odo dev is stopped", func() { BeforeEach(func() { devSession.Stop() @@ -274,6 +269,62 @@ ComponentSettings: }) }) + When("odo dev is executed and Ephemeral is set to false", func() { + + var devSession helper.DevSession + BeforeEach(func() { + helper.Cmd("odo", "preference", "set", "-f", "Ephemeral", "false").ShouldPass() + var err error + devSession, _, _, _, err = helper.StartDevMode(nil) + Expect(err).ToNot(HaveOccurred()) + }) + + When("killing odo dev and running odo delete component --wait", func() { + BeforeEach(func() { + devSession.Kill() + devSession.WaitEnd() + helper.Cmd("odo", "delete", "component", "--wait", "-f").ShouldPass() + }) + + It("should have deleted all resources before returning", func() { + By("deleting the service", func() { + services := commonVar.CliRunner.GetServices(commonVar.Project) + Expect(services).To(BeEmpty()) + }) + By("deleting the PVC", func() { + pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project) + Expect(pvcs).To(BeEmpty()) + }) + By("deleting the pod", func() { + pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project) + Expect(pods).To(BeEmpty()) + }) + }) + }) + + When("stopping odo dev normally", func() { + BeforeEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should have deleted all resources before returning", func() { + By("deleting the service", func() { + services := commonVar.CliRunner.GetServices(commonVar.Project) + Expect(services).To(BeEmpty()) + }) + By("deleting the PVC", func() { + pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project) + Expect(pvcs).To(BeEmpty()) + }) + By("deleting the pod", func() { + pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project) + Expect(pods).To(BeEmpty()) + }) + }) + }) + }) + When("odo dev is executed and Ephemeral is set to false", func() { var devSession helper.DevSession @@ -319,51 +370,6 @@ ComponentSettings: Expect(string(output)).To(ContainSubstring(fmt.Sprintf("Deployment/%s-app", cmpName))) }) }) - - When("killing odo dev and running odo delete component --wait", func() { - BeforeEach(func() { - devSession.Kill() - devSession.WaitEnd() - helper.Cmd("odo", "delete", "component", "--wait", "-f").ShouldPass() - }) - - It("should have deleted all resources before returning", func() { - By("deleting the service", func() { - services := commonVar.CliRunner.GetServices(commonVar.Project) - Expect(services).To(BeEmpty()) - }) - By("deleting the PVC", func() { - pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project) - Expect(pvcs).To(BeEmpty()) - }) - By("deleting the pod", func() { - pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project) - Expect(pods).To(BeEmpty()) - }) - }) - }) - - When("stopping odo dev normally", func() { - BeforeEach(func() { - devSession.Stop() - devSession.WaitEnd() - }) - - It("should have deleted all resources before returning", func() { - By("deleting the service", func() { - services := commonVar.CliRunner.GetServices(commonVar.Project) - Expect(services).To(BeEmpty()) - }) - By("deleting the PVC", func() { - pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project) - Expect(pvcs).To(BeEmpty()) - }) - By("deleting the pod", func() { - pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project) - Expect(pods).To(BeEmpty()) - }) - }) - }) }) When("odo is executed with --no-watch flag", func() { @@ -382,14 +388,37 @@ ComponentSettings: }) When("a file in component directory is modified", func() { - It("should not trigger a push", func() { + + BeforeEach(func() { helper.ReplaceString(filepath.Join(commonVar.Context, "server.js"), "App started", "App is super started") + }) + + It("should not trigger a push", func() { podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project) execResult := commonVar.CliRunner.Exec(podName, commonVar.Project, "cat", "/projects/server.js") Expect(execResult).To(ContainSubstring("App started")) Expect(execResult).ToNot(ContainSubstring("App is super started")) }) + + When("p is pressed", func() { + + BeforeEach(func() { + if os.Getenv("SKIP_KEY_PRESS") == "true" { + Skip("This is a unix-terminal specific scenario, skipping") + } + + devSession.PressKey('p') + }) + + It("should trigger a push", func() { + _, _, _, err := devSession.WaitSync() + Expect(err).ToNot(HaveOccurred()) + podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project) + execResult := commonVar.CliRunner.Exec(podName, commonVar.Project, "cat", "/projects/server.js") + Expect(execResult).To(ContainSubstring("App is super started")) + }) + }) }) }) @@ -435,152 +464,113 @@ ComponentSettings: }) }) - Context("port-forwarding for the component", func() { - When("devfile has single endpoint", func() { - BeforeEach(func() { - helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) - helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass() - helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass() - }) - - When("running odo dev", func() { - var devSession helper.DevSession - var ports map[string]string + for _, manual := range []bool{false, true} { + manual := manual + Context("port-forwarding for the component", func() { + When("devfile has single endpoint", func() { BeforeEach(func() { - var err error - devSession, _, _, ports, err = helper.StartDevMode(nil) - Expect(err).ToNot(HaveOccurred()) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) + helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass() + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass() }) - AfterEach(func() { - devSession.Stop() - devSession.WaitEnd() - }) - - It("should expose the endpoint on localhost", func() { - url := fmt.Sprintf("http://%s", ports["3000"]) - resp, err := http.Get(url) - Expect(err).ToNot(HaveOccurred()) - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) - Expect(err).ToNot(HaveOccurred()) - }) - - When("modifying memoryLimit for container in Devfile", func() { + When("running odo dev", func() { + var devSession helper.DevSession + var ports map[string]string BeforeEach(func() { - src := "memoryLimit: 1024Mi" - dst := "memoryLimit: 1023Mi" - helper.ReplaceString("devfile.yaml", src, dst) var err error - _, _, ports, err = devSession.WaitSync() - Expect(err).Should(Succeed()) + opts := []string{} + if manual { + opts = append(opts, "--no-watch") + } + devSession, _, _, ports, err = helper.StartDevMode(nil, opts...) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() }) It("should expose the endpoint on localhost", func() { - By("updating the pod", func() { - podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project) - bufferOutput := commonVar.CliRunner.Run("get", "pods", podName, "-o", "jsonpath='{.spec.containers[0].resources.requests.memory}'").Out.Contents() - output := string(bufferOutput) - Expect(output).To(ContainSubstring("1023Mi")) + url := fmt.Sprintf("http://%s", ports["3000"]) + resp, err := http.Get(url) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) + Expect(err).ToNot(HaveOccurred()) + }) + + When("modifying memoryLimit for container in Devfile", func() { + BeforeEach(func() { + src := "memoryLimit: 1024Mi" + dst := "memoryLimit: 1023Mi" + helper.ReplaceString("devfile.yaml", src, dst) + if manual { + if os.Getenv("SKIP_KEY_PRESS") == "true" { + Skip("This is a unix-terminal specific scenario, skipping") + } + + devSession.PressKey('p') + } + var err error + _, _, ports, err = devSession.WaitSync() + Expect(err).Should(Succeed()) }) - By("exposing the endpoint", func() { - url := fmt.Sprintf("http://%s", ports["3000"]) - resp, err := http.Get(url) - Expect(err).ToNot(HaveOccurred()) - defer resp.Body.Close() + It("should expose the endpoint on localhost", func() { + By("updating the pod", func() { + podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project) + bufferOutput := commonVar.CliRunner.Run("get", "pods", podName, "-o", "jsonpath='{.spec.containers[0].resources.requests.memory}'").Out.Contents() + output := string(bufferOutput) + Expect(output).To(ContainSubstring("1023Mi")) + }) - body, _ := io.ReadAll(resp.Body) - helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) - Expect(err).ToNot(HaveOccurred()) + By("exposing the endpoint", func() { + url := fmt.Sprintf("http://%s", ports["3000"]) + resp, err := http.Get(url) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) + Expect(err).ToNot(HaveOccurred()) + }) }) }) }) }) - }) - When("devfile has multiple endpoints", func() { - BeforeEach(func() { - helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context) - helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass() - helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass() - }) - - When("running odo dev", func() { - var devSession helper.DevSession - var ports map[string]string + When("devfile has multiple endpoints", func() { BeforeEach(func() { - var err error - devSession, _, _, ports, err = helper.StartDevMode(nil) - Expect(err).ToNot(HaveOccurred()) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context) + helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass() + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass() }) - AfterEach(func() { - devSession.Stop() - devSession.WaitEnd() - }) - - It("should expose two endpoints on localhost", func() { - url1 := fmt.Sprintf("http://%s", ports["3000"]) - url2 := fmt.Sprintf("http://%s", ports["4567"]) - - resp1, err := http.Get(url1) - Expect(err).ToNot(HaveOccurred()) - defer resp1.Body.Close() - - resp2, err := http.Get(url2) - Expect(err).ToNot(HaveOccurred()) - defer resp2.Body.Close() - - body1, _ := io.ReadAll(resp1.Body) - helper.MatchAllInOutput(string(body1), []string{"Hello from Node.js Starter Application!"}) - - body2, _ := io.ReadAll(resp2.Body) - helper.MatchAllInOutput(string(body2), []string{"Hello from Node.js Starter Application!"}) - - helper.ReplaceString("server.js", "Hello from Node.js", "H3110 from Node.js") - - _, _, _, err = devSession.WaitSync() - Expect(err).Should(Succeed()) - - Eventually(func() bool { - resp3, err := http.Get(url1) - if err != nil { - return false - } - defer resp3.Body.Close() - - resp4, err := http.Get(url2) - if err != nil { - return false - } - defer resp4.Body.Close() - - body3, _ := io.ReadAll(resp3.Body) - if string(body3) != "H3110 from Node.js Starter Application!" { - return false - } - - body4, _ := io.ReadAll(resp4.Body) - return string(body4) == "H3110 from Node.js Starter Application!" - }, 180, 10).Should(Equal(true)) - }) - - When("an endpoint is added after first run of odo dev", func() { - + When("running odo dev", func() { + var devSession helper.DevSession + var ports map[string]string BeforeEach(func() { - helper.ReplaceString("devfile.yaml", "exposure: none", "exposure: public") + opts := []string{} + if manual { + opts = append(opts, "--no-watch") + } var err error - _, _, ports, err = devSession.WaitSync() - Expect(err).Should(Succeed()) - + devSession, _, _, ports, err = helper.StartDevMode(nil, opts...) + Expect(err).ToNot(HaveOccurred()) }) - It("should expose three endpoints on localhost", func() { + + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should expose two endpoints on localhost", func() { url1 := fmt.Sprintf("http://%s", ports["3000"]) url2 := fmt.Sprintf("http://%s", ports["4567"]) - url3 := fmt.Sprintf("http://%s", ports["7890"]) resp1, err := http.Get(url1) Expect(err).ToNot(HaveOccurred()) @@ -590,24 +580,98 @@ ComponentSettings: Expect(err).ToNot(HaveOccurred()) defer resp2.Body.Close() - resp3, err := http.Get(url3) - Expect(err).ToNot(HaveOccurred()) - defer resp3.Body.Close() - body1, _ := io.ReadAll(resp1.Body) helper.MatchAllInOutput(string(body1), []string{"Hello from Node.js Starter Application!"}) body2, _ := io.ReadAll(resp2.Body) helper.MatchAllInOutput(string(body2), []string{"Hello from Node.js Starter Application!"}) - body3, _ := io.ReadAll(resp3.Body) - helper.MatchAllInOutput(string(body3), []string{"Hello from Node.js Starter Application!"}) + helper.ReplaceString("server.js", "Hello from Node.js", "H3110 from Node.js") + + if manual { + if os.Getenv("SKIP_KEY_PRESS") == "true" { + Skip("This is a unix-terminal specific scenario, skipping") + } + + devSession.PressKey('p') + } + + _, _, _, err = devSession.WaitSync() + Expect(err).Should(Succeed()) + + Eventually(func() bool { + resp3, err := http.Get(url1) + if err != nil { + return false + } + defer resp3.Body.Close() + + resp4, err := http.Get(url2) + if err != nil { + return false + } + defer resp4.Body.Close() + + body3, _ := io.ReadAll(resp3.Body) + if string(body3) != "H3110 from Node.js Starter Application!" { + return false + } + + body4, _ := io.ReadAll(resp4.Body) + return string(body4) == "H3110 from Node.js Starter Application!" + }, 180, 10).Should(Equal(true)) + }) + + When("an endpoint is added after first run of odo dev", func() { + + BeforeEach(func() { + helper.ReplaceString("devfile.yaml", "exposure: none", "exposure: public") + + if manual { + if os.Getenv("SKIP_KEY_PRESS") == "true" { + Skip("This is a unix-terminal specific scenario, skipping") + } + + devSession.PressKey('p') + } + + var err error + _, _, ports, err = devSession.WaitSync() + Expect(err).Should(Succeed()) + + }) + It("should expose three endpoints on localhost", func() { + url1 := fmt.Sprintf("http://%s", ports["3000"]) + url2 := fmt.Sprintf("http://%s", ports["4567"]) + url3 := fmt.Sprintf("http://%s", ports["7890"]) + + resp1, err := http.Get(url1) + Expect(err).ToNot(HaveOccurred()) + defer resp1.Body.Close() + + resp2, err := http.Get(url2) + Expect(err).ToNot(HaveOccurred()) + defer resp2.Body.Close() + + resp3, err := http.Get(url3) + Expect(err).ToNot(HaveOccurred()) + defer resp3.Body.Close() + + body1, _ := io.ReadAll(resp1.Body) + helper.MatchAllInOutput(string(body1), []string{"Hello from Node.js Starter Application!"}) + + body2, _ := io.ReadAll(resp2.Body) + helper.MatchAllInOutput(string(body2), []string{"Hello from Node.js Starter Application!"}) + + body3, _ := io.ReadAll(resp3.Body) + helper.MatchAllInOutput(string(body3), []string{"Hello from Node.js Starter Application!"}) + }) }) }) - }) + }) }) - }) + } for _, devfileHandlerCtx := range []struct { name string @@ -1292,10 +1356,6 @@ ComponentSettings: session, sessionOut, sessionErr, ports, err = helper.StartDevMode([]string{"PODMAN_CMD=echo"}) Expect(err).ToNot(HaveOccurred()) }) - AfterEach(func() { - session.Stop() - session.WaitEnd() - }) It("should execute the composite apply commands successfully", func() { checkDeploymentExists := func() { out := commonVar.CliRunner.Run("get", "deployments", deploymentName).Out.Contents() @@ -2341,6 +2401,34 @@ CMD ["npm", "start"] }) } + When("a component with multiple endpoints is run", func() { + stateFile := ".odo/devstate.json" + var devSession helper.DevSession + BeforeEach(func() { + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context) + helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass() + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass() + Expect(helper.VerifyFileExists(".odo/devstate.json")).To(BeFalse()) + var err error + devSession, _, _, _, err = helper.StartDevMode(nil) + Expect(err).ToNot(HaveOccurred()) + }) + + When("odo dev is stopped", func() { + BeforeEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should remove forwarded ports from state file", func() { + Expect(helper.VerifyFileExists(stateFile)).To(BeTrue()) + contentJSON, err := ioutil.ReadFile(stateFile) + Expect(err).ToNot(HaveOccurred()) + helper.JsonPathContentIs(string(contentJSON), "forwardedPorts", "") + }) + }) + }) + When("a component with multiple endpoints is run", func() { stateFile := ".odo/devstate.json" var devSession helper.DevSession @@ -2373,20 +2461,6 @@ CMD ["npm", "start"] helper.JsonPathContentIsValidUserPort(string(contentJSON), "forwardedPorts.0.localPort") helper.JsonPathContentIsValidUserPort(string(contentJSON), "forwardedPorts.1.localPort") }) - - When("odo dev is stopped", func() { - BeforeEach(func() { - devSession.Stop() - devSession.WaitEnd() - }) - - It("should remove forwarded ports from state file", func() { - Expect(helper.VerifyFileExists(stateFile)).To(BeTrue()) - contentJSON, err := ioutil.ReadFile(stateFile) - Expect(err).ToNot(HaveOccurred()) - helper.JsonPathContentIs(string(contentJSON), "forwardedPorts", "") - }) - }) }) When("a devfile with a local parent is used for odo dev and the parent is not synced", func() { diff --git a/tests/integration/interactive_dev_test.go b/tests/integration/interactive_dev_test.go index ebefee333..17ecafd6d 100644 --- a/tests/integration/interactive_dev_test.go +++ b/tests/integration/interactive_dev_test.go @@ -60,7 +60,7 @@ var _ = Describe("odo dev interactive command tests", func() { helper.ExpectString(ctx, "Enter component name") helper.SendLine(ctx, "my-app") - helper.ExpectString(ctx, "Press Ctrl+c to exit") + helper.ExpectString(ctx, "[Ctrl+c] - Exit") ctx.StopCommand() }) @@ -91,7 +91,7 @@ var _ = Describe("odo dev interactive command tests", func() { helper.ExpectString(ctx, "Enter component name") helper.SendLine(ctx, "my-app") - helper.ExpectString(ctx, "Press Ctrl+c to exit") + helper.ExpectString(ctx, "[Ctrl+c] - Exit") ctx.StopCommand() }) @@ -129,7 +129,7 @@ var _ = Describe("odo dev interactive command tests", func() { helper.ExpectString(ctx, "Enter component name") helper.SendLine(ctx, "my-app") - helper.ExpectString(ctx, "Press Ctrl+c to exit") + helper.ExpectString(ctx, "[Ctrl+c] - Exit") ctx.StopCommand() }) @@ -140,4 +140,28 @@ var _ = Describe("odo dev interactive command tests", func() { "odo will try to autodetect the language and project type in order to select the best suited Devfile for your project.")) }) }) + + When("a component is bootstrapped", func() { + + var cmpName string + + BeforeEach(func() { + cmpName = helper.RandString(6) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) + helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass() + }) + + It("should sync files when p is pressed", func() { + _, _ = helper.RunInteractive([]string{"odo", "dev", "--random-ports", "--no-watch"}, + nil, + func(ctx helper.InteractiveContext) { + helper.ExpectString(ctx, "[p] - Manually apply") + + helper.PressKey(ctx, 'p') + + helper.ExpectString(ctx, "Pushing files") + ctx.StopCommand() + }) + }) + }) }) diff --git a/vendor/github.com/hinshun/vt10x/.travis.yml b/vendor/github.com/hinshun/vt10x/.travis.yml deleted file mode 100644 index 8c2998f7d..000000000 --- a/vendor/github.com/hinshun/vt10x/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: go - -go: - - "1.10.2" - - master diff --git a/vendor/github.com/hinshun/vt10x/LICENSE b/vendor/github.com/hinshun/vt10x/LICENSE deleted file mode 100644 index a5976d65d..000000000 --- a/vendor/github.com/hinshun/vt10x/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2013 James Gray - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without liitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and thismssion notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/hinshun/vt10x/README.md b/vendor/github.com/hinshun/vt10x/README.md deleted file mode 100644 index 420318f67..000000000 --- a/vendor/github.com/hinshun/vt10x/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# vt10x - -[![Build Status](https://travis-ci.org/hinshun/vt10x.svg?branch=master)](https://travis-ci.org/hinshun/vt10x) -[![GoDoc](https://godoc.org/github.com/hinshun/vt10x?status.svg)](https://godoc.org/github.com/hinshun/vt10x) - -Package vt10x is a vt10x terminal emulation backend, influenced -largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal -muxing, a terminal emulation frontend, or wherever else you need -terminal emulation. diff --git a/vendor/github.com/hinshun/vt10x/color.go b/vendor/github.com/hinshun/vt10x/color.go deleted file mode 100644 index c16d3ac4a..000000000 --- a/vendor/github.com/hinshun/vt10x/color.go +++ /dev/null @@ -1,38 +0,0 @@ -package vt10x - -// ANSI color values -const ( - Black Color = iota - Red - Green - Yellow - Blue - Magenta - Cyan - LightGrey - DarkGrey - LightRed - LightGreen - LightYellow - LightBlue - LightMagenta - LightCyan - White -) - -// Default colors are potentially distinct to allow for special behavior. -// For example, a transparent background. Otherwise, the simple case is to -// map default colors to another color. -const ( - DefaultFG Color = 1<<24 + iota - DefaultBG - DefaultCursor -) - -// Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256). -type Color uint32 - -// ANSI returns true if Color is within [0, 16). -func (c Color) ANSI() bool { - return (c < 16) -} diff --git a/vendor/github.com/hinshun/vt10x/csi.go b/vendor/github.com/hinshun/vt10x/csi.go deleted file mode 100644 index f5df174ec..000000000 --- a/vendor/github.com/hinshun/vt10x/csi.go +++ /dev/null @@ -1,189 +0,0 @@ -package vt10x - -import ( - "fmt" - "strconv" - "strings" -) - -// CSI (Control Sequence Introducer) -// ESC+[ -type csiEscape struct { - buf []byte - args []int - mode byte - priv bool -} - -func (c *csiEscape) reset() { - c.buf = c.buf[:0] - c.args = c.args[:0] - c.mode = 0 - c.priv = false -} - -func (c *csiEscape) put(b byte) bool { - c.buf = append(c.buf, b) - if b >= 0x40 && b <= 0x7E || len(c.buf) >= 256 { - c.parse() - return true - } - return false -} - -func (c *csiEscape) parse() { - c.mode = c.buf[len(c.buf)-1] - if len(c.buf) == 1 { - return - } - s := string(c.buf) - c.args = c.args[:0] - if s[0] == '?' { - c.priv = true - s = s[1:] - } - s = s[:len(s)-1] - ss := strings.Split(s, ";") - for _, p := range ss { - i, err := strconv.Atoi(p) - if err != nil { - //t.logf("invalid CSI arg '%s'\n", p) - break - } - c.args = append(c.args, i) - } -} - -func (c *csiEscape) arg(i, def int) int { - if i >= len(c.args) || i < 0 { - return def - } - return c.args[i] -} - -// maxarg takes the maximum of arg(i, def) and def -func (c *csiEscape) maxarg(i, def int) int { - return max(c.arg(i, def), def) -} - -func (t *State) handleCSI() { - c := &t.csi - switch c.mode { - default: - goto unknown - case '@': // ICH - insert blank char - t.insertBlanks(c.arg(0, 1)) - case 'A': // CUU - cursor up - t.moveTo(t.cur.X, t.cur.Y-c.maxarg(0, 1)) - case 'B', 'e': // CUD, VPR - cursor down - t.moveTo(t.cur.X, t.cur.Y+c.maxarg(0, 1)) - case 'c': // DA - device attributes - if c.arg(0, 0) == 0 { - // TODO: write vt102 id - } - case 'C', 'a': // CUF, HPR - cursor forward - t.moveTo(t.cur.X+c.maxarg(0, 1), t.cur.Y) - case 'D': // CUB - cursor backward - t.moveTo(t.cur.X-c.maxarg(0, 1), t.cur.Y) - case 'E': // CNL - cursor down and first col - t.moveTo(0, t.cur.Y+c.arg(0, 1)) - case 'F': // CPL - cursor up and first col - t.moveTo(0, t.cur.Y-c.arg(0, 1)) - case 'g': // TBC - tabulation clear - switch c.arg(0, 0) { - // clear current tab stop - case 0: - t.tabs[t.cur.X] = false - // clear all tabs - case 3: - for i := range t.tabs { - t.tabs[i] = false - } - default: - goto unknown - } - case 'G', '`': // CHA, HPA - Move to - t.moveTo(c.arg(0, 1)-1, t.cur.Y) - case 'H', 'f': // CUP, HVP - move to - t.moveAbsTo(c.arg(1, 1)-1, c.arg(0, 1)-1) - case 'I': // CHT - cursor forward tabulation tab stops - n := c.arg(0, 1) - for i := 0; i < n; i++ { - t.putTab(true) - } - case 'J': // ED - clear screen - // TODO: sel.ob.x = -1 - switch c.arg(0, 0) { - case 0: // below - t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) - if t.cur.Y < t.rows-1 { - t.clear(0, t.cur.Y+1, t.cols-1, t.rows-1) - } - case 1: // above - if t.cur.Y > 1 { - t.clear(0, 0, t.cols-1, t.cur.Y-1) - } - t.clear(0, t.cur.Y, t.cur.X, t.cur.Y) - case 2: // all - t.clear(0, 0, t.cols-1, t.rows-1) - default: - goto unknown - } - case 'K': // EL - clear line - switch c.arg(0, 0) { - case 0: // right - t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) - case 1: // left - t.clear(0, t.cur.Y, t.cur.X, t.cur.Y) - case 2: // all - t.clear(0, t.cur.Y, t.cols-1, t.cur.Y) - } - case 'S': // SU - scroll lines up - t.scrollUp(t.top, c.arg(0, 1)) - case 'T': // SD - scroll lines down - t.scrollDown(t.top, c.arg(0, 1)) - case 'L': // IL - insert blank lines - t.insertBlankLines(c.arg(0, 1)) - case 'l': // RM - reset mode - t.setMode(c.priv, false, c.args) - case 'M': // DL - delete lines - t.deleteLines(c.arg(0, 1)) - case 'X': // ECH - erase chars - t.clear(t.cur.X, t.cur.Y, t.cur.X+c.arg(0, 1)-1, t.cur.Y) - case 'P': // DCH - delete chars - t.deleteChars(c.arg(0, 1)) - case 'Z': // CBT - cursor backward tabulation tab stops - n := c.arg(0, 1) - for i := 0; i < n; i++ { - t.putTab(false) - } - case 'd': // VPA - move to - t.moveAbsTo(t.cur.X, c.arg(0, 1)-1) - case 'h': // SM - set terminal mode - t.setMode(c.priv, true, c.args) - case 'm': // SGR - terminal attribute (color) - t.setAttr(c.args) - case 'n': - switch c.arg(0, 0) { - case 5: // DSR - device status report - t.w.Write([]byte("\033[0n")) - case 6: // CPR - cursor position report - t.w.Write([]byte(fmt.Sprintf("\033[%d;%dR", t.cur.Y+1, t.cur.X+1))) - } - case 'r': // DECSTBM - set scrolling region - if c.priv { - goto unknown - } else { - t.setScroll(c.arg(0, 1)-1, c.arg(1, t.rows)-1) - t.moveAbsTo(0, 0) - } - case 's': // DECSC - save cursor position (ANSI.SYS) - t.saveCursor() - case 'u': // DECRC - restore cursor position (ANSI.SYS) - t.restoreCursor() - } - return -unknown: // TODO: get rid of this goto - t.logf("unknown CSI sequence '%c'\n", c.mode) - // TODO: c.dump() -} diff --git a/vendor/github.com/hinshun/vt10x/doc.go b/vendor/github.com/hinshun/vt10x/doc.go deleted file mode 100644 index 8205207d6..000000000 --- a/vendor/github.com/hinshun/vt10x/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -/* -Package terminal is a vt10x terminal emulation backend, influenced -largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal -muxing, a terminal emulation frontend, or wherever else you need -terminal emulation. - -In development, but very usable. -*/ -package vt10x diff --git a/vendor/github.com/hinshun/vt10x/ioctl_other.go b/vendor/github.com/hinshun/vt10x/ioctl_other.go deleted file mode 100644 index 0aa186875..000000000 --- a/vendor/github.com/hinshun/vt10x/ioctl_other.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build plan9 nacl windows - -package vt10x - -import ( - "os" -) - -func ioctl(f *os.File, cmd, p uintptr) error { - return nil -} - -func ResizePty(*os.File) error { - return nil -} diff --git a/vendor/github.com/hinshun/vt10x/ioctl_posix.go b/vendor/github.com/hinshun/vt10x/ioctl_posix.go deleted file mode 100644 index 7b81b3a1c..000000000 --- a/vendor/github.com/hinshun/vt10x/ioctl_posix.go +++ /dev/null @@ -1,31 +0,0 @@ -// +build linux darwin dragonfly solaris openbsd netbsd freebsd - -package vt10x - -import ( - "os" - "syscall" - "unsafe" -) - -func ioctl(f *os.File, cmd, p uintptr) error { - _, _, errno := syscall.Syscall( - syscall.SYS_IOCTL, - f.Fd(), - syscall.TIOCSWINSZ, - p) - if errno != 0 { - return syscall.Errno(errno) - } - return nil -} - -func ResizePty(pty *os.File, cols, rows int) error { - var w struct{ row, col, xpix, ypix uint16 } - w.row = uint16(rows) - w.col = uint16(cols) - w.xpix = 16 * uint16(cols) - w.ypix = 16 * uint16(rows) - return ioctl(pty, syscall.TIOCSWINSZ, - uintptr(unsafe.Pointer(&w))) -} diff --git a/vendor/github.com/hinshun/vt10x/parse.go b/vendor/github.com/hinshun/vt10x/parse.go deleted file mode 100644 index 0b841457c..000000000 --- a/vendor/github.com/hinshun/vt10x/parse.go +++ /dev/null @@ -1,203 +0,0 @@ -package vt10x - -func isControlCode(c rune) bool { - return c < 0x20 || c == 0177 -} - -func (t *State) parse(c rune) { - t.logf("%q", string(c)) - if isControlCode(c) { - if t.handleControlCodes(c) || t.cur.Attr.Mode&attrGfx == 0 { - return - } - } - // TODO: update selection; see st.c:2450 - - if t.mode&ModeWrap != 0 && t.cur.State&cursorWrapNext != 0 { - t.lines[t.cur.Y][t.cur.X].Mode |= attrWrap - t.newline(true) - } - - if t.mode&ModeInsert != 0 && t.cur.X+1 < t.cols { - // TODO: move shiz, look at st.c:2458 - t.logln("insert mode not implemented") - } - - t.setChar(c, &t.cur.Attr, t.cur.X, t.cur.Y) - if t.cur.X+1 < t.cols { - t.moveTo(t.cur.X+1, t.cur.Y) - } else { - t.cur.State |= cursorWrapNext - } -} - -func (t *State) parseEsc(c rune) { - if t.handleControlCodes(c) { - return - } - next := t.parse - t.logf("%q", string(c)) - switch c { - case '[': - next = t.parseEscCSI - case '#': - next = t.parseEscTest - case 'P', // DCS - Device Control String - '_', // APC - Application Program Command - '^', // PM - Privacy Message - ']', // OSC - Operating System Command - 'k': // old title set compatibility - t.str.reset() - t.str.typ = c - next = t.parseEscStr - case '(': // set primary charset G0 - next = t.parseEscAltCharset - case ')', // set secondary charset G1 (ignored) - '*', // set tertiary charset G2 (ignored) - '+': // set quaternary charset G3 (ignored) - case 'D': // IND - linefeed - if t.cur.Y == t.bottom { - t.scrollUp(t.top, 1) - } else { - t.moveTo(t.cur.X, t.cur.Y+1) - } - case 'E': // NEL - next line - t.newline(true) - case 'H': // HTS - horizontal tab stop - t.tabs[t.cur.X] = true - case 'M': // RI - reverse index - if t.cur.Y == t.top { - t.scrollDown(t.top, 1) - } else { - t.moveTo(t.cur.X, t.cur.Y-1) - } - case 'Z': // DECID - identify terminal - // TODO: write to our writer our id - case 'c': // RIS - reset to initial state - t.reset() - case '=': // DECPAM - application keypad - t.mode |= ModeAppKeypad - case '>': // DECPNM - normal keypad - t.mode &^= ModeAppKeypad - case '7': // DECSC - save cursor - t.saveCursor() - case '8': // DECRC - restore cursor - t.restoreCursor() - case '\\': // ST - stop - default: - t.logf("unknown ESC sequence '%c'\n", c) - } - t.state = next -} - -func (t *State) parseEscCSI(c rune) { - if t.handleControlCodes(c) { - return - } - t.logf("%q", string(c)) - if t.csi.put(byte(c)) { - t.state = t.parse - t.handleCSI() - } -} - -func (t *State) parseEscStr(c rune) { - t.logf("%q", string(c)) - switch c { - case '\033': - t.state = t.parseEscStrEnd - case '\a': // backwards compatiblity to xterm - t.state = t.parse - t.handleSTR() - default: - t.str.put(c) - } -} - -func (t *State) parseEscStrEnd(c rune) { - if t.handleControlCodes(c) { - return - } - t.logf("%q", string(c)) - t.state = t.parse - if c == '\\' { - t.handleSTR() - } -} - -func (t *State) parseEscAltCharset(c rune) { - if t.handleControlCodes(c) { - return - } - t.logf("%q", string(c)) - switch c { - case '0': // line drawing set - t.cur.Attr.Mode |= attrGfx - case 'B': // USASCII - t.cur.Attr.Mode &^= attrGfx - case 'A', // UK (ignored) - '<', // multinational (ignored) - '5', // Finnish (ignored) - 'C', // Finnish (ignored) - 'K': // German (ignored) - default: - t.logf("unknown alt. charset '%c'\n", c) - } - t.state = t.parse -} - -func (t *State) parseEscTest(c rune) { - if t.handleControlCodes(c) { - return - } - // DEC screen alignment test - if c == '8' { - for y := 0; y < t.rows; y++ { - for x := 0; x < t.cols; x++ { - t.setChar('E', &t.cur.Attr, x, y) - } - } - } - t.state = t.parse -} - -func (t *State) handleControlCodes(c rune) bool { - if !isControlCode(c) { - return false - } - switch c { - // HT - case '\t': - t.putTab(true) - // BS - case '\b': - t.moveTo(t.cur.X-1, t.cur.Y) - // CR - case '\r': - t.moveTo(0, t.cur.Y) - // LF, VT, LF - case '\f', '\v', '\n': - // go to first col if mode is set - t.newline(t.mode&ModeCRLF != 0) - // BEL - case '\a': - // TODO: emit sound - // TODO: window alert if not focused - // ESC - case 033: - t.csi.reset() - t.state = t.parseEsc - // SO, SI - case 016, 017: - // different charsets not supported. apps should use the correct - // alt charset escapes, probably for line drawing - // SUB, CAN - case 032, 030: - t.csi.reset() - // ignore ENQ, NUL, XON, XOFF, DEL - case 005, 000, 021, 023, 0177: - default: - return false - } - return true -} diff --git a/vendor/github.com/hinshun/vt10x/state.go b/vendor/github.com/hinshun/vt10x/state.go deleted file mode 100644 index b2b40786d..000000000 --- a/vendor/github.com/hinshun/vt10x/state.go +++ /dev/null @@ -1,762 +0,0 @@ -package vt10x - -import ( - "io" - "log" - "sync" -) - -const ( - tabspaces = 8 -) - -const ( - attrReverse = 1 << iota - attrUnderline - attrBold - attrGfx - attrItalic - attrBlink - attrWrap -) - -const ( - cursorDefault = 1 << iota - cursorWrapNext - cursorOrigin -) - -// ModeFlag represents various terminal mode states. -type ModeFlag uint32 - -// Terminal modes -const ( - ModeWrap ModeFlag = 1 << iota - ModeInsert - ModeAppKeypad - ModeAltScreen - ModeCRLF - ModeMouseButton - ModeMouseMotion - ModeReverse - ModeKeyboardLock - ModeHide - ModeEcho - ModeAppCursor - ModeMouseSgr - Mode8bit - ModeBlink - ModeFBlink - ModeFocus - ModeMouseX10 - ModeMouseMany - ModeMouseMask = ModeMouseButton | ModeMouseMotion | ModeMouseX10 | ModeMouseMany -) - -// ChangeFlag represents possible state changes of the terminal. -type ChangeFlag uint32 - -// Terminal changes to occur in VT.ReadState -const ( - ChangedScreen ChangeFlag = 1 << iota - ChangedTitle -) - -type Glyph struct { - Char rune - Mode int16 - FG, BG Color -} - -type line []Glyph - -type Cursor struct { - Attr Glyph - X, Y int - State uint8 -} - -type parseState func(c rune) - -// State represents the terminal emulation state. Use Lock/Unlock -// methods to synchronize data access with VT. -type State struct { - DebugLogger *log.Logger - - w io.Writer - mu sync.Mutex - changed ChangeFlag - cols, rows int - lines []line - altLines []line - dirty []bool // line dirtiness - anydirty bool - cur, curSaved Cursor - top, bottom int // scroll limits - mode ModeFlag - state parseState - str strEscape - csi csiEscape - numlock bool - tabs []bool - title string - colorOverride map[Color]Color -} - -func newState(w io.Writer) *State { - return &State{ - w: w, - colorOverride: make(map[Color]Color), - } -} - -func (t *State) logf(format string, args ...interface{}) { - if t.DebugLogger != nil { - t.DebugLogger.Printf(format, args...) - } -} - -func (t *State) logln(s string) { - if t.DebugLogger != nil { - t.DebugLogger.Println(s) - } -} - -func (t *State) lock() { - t.mu.Lock() -} - -func (t *State) unlock() { - t.mu.Unlock() -} - -// Lock locks the state object's mutex. -func (t *State) Lock() { - t.mu.Lock() -} - -// Unlock resets change flags and unlocks the state object's mutex. -func (t *State) Unlock() { - t.resetChanges() - t.mu.Unlock() -} - -// Cell returns the glyph containing the character code, foreground color, and -// background color at position (x, y) relative to the top left of the terminal. -func (t *State) Cell(x, y int) Glyph { - cell := t.lines[y][x] - fg, ok := t.colorOverride[cell.FG] - if ok { - cell.FG = fg - } - bg, ok := t.colorOverride[cell.BG] - if ok { - cell.BG = bg - } - return cell -} - -// Cursor returns the current position of the cursor. -func (t *State) Cursor() Cursor { - return t.cur -} - -// CursorVisible returns the visible state of the cursor. -func (t *State) CursorVisible() bool { - return t.mode&ModeHide == 0 -} - -// Mode returns the current terminal mode. -func (t *State) Mode() ModeFlag { - return t.mode -} - -// Title returns the current title set via the tty. -func (t *State) Title() string { - return t.title -} - -/* -// ChangeMask returns a bitfield of changes that have occured by VT. -func (t *State) ChangeMask() ChangeFlag { - return t.changed -} -*/ - -// Changed returns true if change has occured. -func (t *State) Changed(change ChangeFlag) bool { - return t.changed&change != 0 -} - -// resetChanges resets the change mask and dirtiness. -func (t *State) resetChanges() { - for i := range t.dirty { - t.dirty[i] = false - } - t.anydirty = false - t.changed = 0 -} - -func (t *State) saveCursor() { - t.curSaved = t.cur -} - -func (t *State) restoreCursor() { - t.cur = t.curSaved - t.moveTo(t.cur.X, t.cur.Y) -} - -func (t *State) put(c rune) { - t.state(c) -} - -func (t *State) putTab(forward bool) { - x := t.cur.X - if forward { - if x == t.cols { - return - } - for x++; x < t.cols && !t.tabs[x]; x++ { - } - } else { - if x == 0 { - return - } - for x--; x > 0 && !t.tabs[x]; x-- { - } - } - t.moveTo(x, t.cur.Y) -} - -func (t *State) newline(firstCol bool) { - y := t.cur.Y - if y == t.bottom { - cur := t.cur - t.cur = t.defaultCursor() - t.scrollUp(t.top, 1) - t.cur = cur - } else { - y++ - } - if firstCol { - t.moveTo(0, y) - } else { - t.moveTo(t.cur.X, y) - } -} - -// table from st, which in turn is from rxvt :) -var gfxCharTable = [62]rune{ - '↑', '↓', '→', '←', '█', '▚', '☃', // A - G - 0, 0, 0, 0, 0, 0, 0, 0, // H - O - 0, 0, 0, 0, 0, 0, 0, 0, // P - W - 0, 0, 0, 0, 0, 0, 0, ' ', // X - _ - '◆', '▒', '␉', '␌', '␍', '␊', '°', '±', // ` - g - '␤', '␋', '┘', '┐', '┌', '└', '┼', '⎺', // h - o - '⎻', '─', '⎼', '⎽', '├', '┤', '┴', '┬', // p - w - '│', '≤', '≥', 'π', '≠', '£', '·', // x - ~ -} - -func (t *State) setChar(c rune, attr *Glyph, x, y int) { - if attr.Mode&attrGfx != 0 { - if c >= 0x41 && c <= 0x7e && gfxCharTable[c-0x41] != 0 { - c = gfxCharTable[c-0x41] - } - } - t.changed |= ChangedScreen - t.dirty[y] = true - t.lines[y][x] = *attr - t.lines[y][x].Char = c - //if t.options.BrightBold && attr.Mode&attrBold != 0 && attr.FG < 8 { - if attr.Mode&attrBold != 0 && attr.FG < 8 { - t.lines[y][x].FG = attr.FG + 8 - } - if attr.Mode&attrReverse != 0 { - t.lines[y][x].FG = attr.BG - t.lines[y][x].BG = attr.FG - } -} - -func (t *State) defaultCursor() Cursor { - c := Cursor{} - c.Attr.FG = DefaultFG - c.Attr.BG = DefaultBG - return c -} - -func (t *State) reset() { - t.cur = t.defaultCursor() - t.saveCursor() - for i := range t.tabs { - t.tabs[i] = false - } - for i := tabspaces; i < len(t.tabs); i += tabspaces { - t.tabs[i] = true - } - t.top = 0 - t.bottom = t.rows - 1 - t.mode = ModeWrap - t.clear(0, 0, t.rows-1, t.cols-1) - t.moveTo(0, 0) -} - -// TODO: definitely can improve allocs -func (t *State) resize(cols, rows int) bool { - if cols == t.cols && rows == t.rows { - return false - } - if cols < 1 || rows < 1 { - return false - } - slide := t.cur.Y - rows + 1 - if slide > 0 { - copy(t.lines, t.lines[slide:slide+rows]) - copy(t.altLines, t.altLines[slide:slide+rows]) - } - - lines, altLines, tabs := t.lines, t.altLines, t.tabs - t.lines = make([]line, rows) - t.altLines = make([]line, rows) - t.dirty = make([]bool, rows) - t.tabs = make([]bool, cols) - - minrows := min(rows, t.rows) - mincols := min(cols, t.cols) - t.changed |= ChangedScreen - for i := 0; i < rows; i++ { - t.dirty[i] = true - t.lines[i] = make(line, cols) - t.altLines[i] = make(line, cols) - } - for i := 0; i < minrows; i++ { - copy(t.lines[i], lines[i]) - copy(t.altLines[i], altLines[i]) - } - copy(t.tabs, tabs) - if cols > t.cols { - i := t.cols - 1 - for i > 0 && !tabs[i] { - i-- - } - for i += tabspaces; i < len(tabs); i += tabspaces { - tabs[i] = true - } - } - - t.cols = cols - t.rows = rows - t.setScroll(0, rows-1) - t.moveTo(t.cur.X, t.cur.Y) - for i := 0; i < 2; i++ { - if mincols < cols && minrows > 0 { - t.clear(mincols, 0, cols-1, minrows-1) - } - if cols > 0 && minrows < rows { - t.clear(0, minrows, cols-1, rows-1) - } - t.swapScreen() - } - return slide > 0 -} - -func (t *State) clear(x0, y0, x1, y1 int) { - if x0 > x1 { - x0, x1 = x1, x0 - } - if y0 > y1 { - y0, y1 = y1, y0 - } - x0 = clamp(x0, 0, t.cols-1) - x1 = clamp(x1, 0, t.cols-1) - y0 = clamp(y0, 0, t.rows-1) - y1 = clamp(y1, 0, t.rows-1) - t.changed |= ChangedScreen - for y := y0; y <= y1; y++ { - t.dirty[y] = true - for x := x0; x <= x1; x++ { - t.lines[y][x] = t.cur.Attr - t.lines[y][x].Char = ' ' - } - } -} - -func (t *State) clearAll() { - t.clear(0, 0, t.cols-1, t.rows-1) -} - -func (t *State) moveAbsTo(x, y int) { - if t.cur.State&cursorOrigin != 0 { - y += t.top - } - t.moveTo(x, y) -} - -func (t *State) moveTo(x, y int) { - var miny, maxy int - if t.cur.State&cursorOrigin != 0 { - miny = t.top - maxy = t.bottom - } else { - miny = 0 - maxy = t.rows - 1 - } - x = clamp(x, 0, t.cols-1) - y = clamp(y, miny, maxy) - t.changed |= ChangedScreen - t.cur.State &^= cursorWrapNext - t.cur.X = x - t.cur.Y = y -} - -func (t *State) swapScreen() { - t.lines, t.altLines = t.altLines, t.lines - t.mode ^= ModeAltScreen - t.dirtyAll() -} - -func (t *State) dirtyAll() { - t.changed |= ChangedScreen - for y := 0; y < t.rows; y++ { - t.dirty[y] = true - } -} - -func (t *State) setScroll(top, bottom int) { - top = clamp(top, 0, t.rows-1) - bottom = clamp(bottom, 0, t.rows-1) - if top > bottom { - top, bottom = bottom, top - } - t.top = top - t.bottom = bottom -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} - -func clamp(val, min, max int) int { - if val < min { - return min - } else if val > max { - return max - } - return val -} - -func between(val, min, max int) bool { - if val < min || val > max { - return false - } - return true -} - -func (t *State) scrollDown(orig, n int) { - n = clamp(n, 0, t.bottom-orig+1) - t.clear(0, t.bottom-n+1, t.cols-1, t.bottom) - t.changed |= ChangedScreen - for i := t.bottom; i >= orig+n; i-- { - t.lines[i], t.lines[i-n] = t.lines[i-n], t.lines[i] - t.dirty[i] = true - t.dirty[i-n] = true - } - - // TODO: selection scroll -} - -func (t *State) scrollUp(orig, n int) { - n = clamp(n, 0, t.bottom-orig+1) - t.clear(0, orig, t.cols-1, orig+n-1) - t.changed |= ChangedScreen - for i := orig; i <= t.bottom-n; i++ { - t.lines[i], t.lines[i+n] = t.lines[i+n], t.lines[i] - t.dirty[i] = true - t.dirty[i+n] = true - } - - // TODO: selection scroll -} - -func (t *State) modMode(set bool, bit ModeFlag) { - if set { - t.mode |= bit - } else { - t.mode &^= bit - } -} - -func (t *State) setMode(priv bool, set bool, args []int) { - if priv { - for _, a := range args { - switch a { - case 1: // DECCKM - cursor key - t.modMode(set, ModeAppCursor) - case 5: // DECSCNM - reverse video - mode := t.mode - t.modMode(set, ModeReverse) - if mode != t.mode { - // TODO: redraw - } - case 6: // DECOM - origin - if set { - t.cur.State |= cursorOrigin - } else { - t.cur.State &^= cursorOrigin - } - t.moveAbsTo(0, 0) - case 7: // DECAWM - auto wrap - t.modMode(set, ModeWrap) - // IGNORED: - case 0, // error - 2, // DECANM - ANSI/VT52 - 3, // DECCOLM - column - 4, // DECSCLM - scroll - 8, // DECARM - auto repeat - 18, // DECPFF - printer feed - 19, // DECPEX - printer extent - 42, // DECNRCM - national characters - 12: // att610 - start blinking cursor - break - case 25: // DECTCEM - text cursor enable mode - t.modMode(!set, ModeHide) - case 9: // X10 mouse compatibility mode - t.modMode(false, ModeMouseMask) - t.modMode(set, ModeMouseX10) - case 1000: // report button press - t.modMode(false, ModeMouseMask) - t.modMode(set, ModeMouseButton) - case 1002: // report motion on button press - t.modMode(false, ModeMouseMask) - t.modMode(set, ModeMouseMotion) - case 1003: // enable all mouse motions - t.modMode(false, ModeMouseMask) - t.modMode(set, ModeMouseMany) - case 1004: // send focus events to tty - t.modMode(set, ModeFocus) - case 1006: // extended reporting mode - t.modMode(set, ModeMouseSgr) - case 1034: - t.modMode(set, Mode8bit) - case 1049, // = 1047 and 1048 - 47, 1047: - alt := t.mode&ModeAltScreen != 0 - if alt { - t.clear(0, 0, t.cols-1, t.rows-1) - } - if !set || !alt { - t.swapScreen() - } - if a != 1049 { - break - } - fallthrough - case 1048: - if set { - t.saveCursor() - } else { - t.restoreCursor() - } - case 1001: - // mouse highlight mode; can hang the terminal by design when - // implemented - case 1005: - // utf8 mouse mode; will confuse applications not supporting - // utf8 and luit - case 1015: - // urxvt mangled mouse mode; incompatiblt and can be mistaken - // for other control codes - default: - t.logf("unknown private set/reset mode %d\n", a) - } - } - } else { - for _, a := range args { - switch a { - case 0: // Error (ignored) - case 2: // KAM - keyboard action - t.modMode(set, ModeKeyboardLock) - case 4: // IRM - insertion-replacement - t.modMode(set, ModeInsert) - t.logln("insert mode not implemented") - case 12: // SRM - send/receive - t.modMode(set, ModeEcho) - case 20: // LNM - linefeed/newline - t.modMode(set, ModeCRLF) - case 34: - t.logln("right-to-left mode not implemented") - case 96: - t.logln("right-to-left copy mode not implemented") - default: - t.logf("unknown set/reset mode %d\n", a) - } - } - } -} - -func (t *State) setAttr(attr []int) { - if len(attr) == 0 { - attr = []int{0} - } - for i := 0; i < len(attr); i++ { - a := attr[i] - switch a { - case 0: - t.cur.Attr.Mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink - t.cur.Attr.FG = DefaultFG - t.cur.Attr.BG = DefaultBG - case 1: - t.cur.Attr.Mode |= attrBold - case 3: - t.cur.Attr.Mode |= attrItalic - case 4: - t.cur.Attr.Mode |= attrUnderline - case 5, 6: // slow, rapid blink - t.cur.Attr.Mode |= attrBlink - case 7: - t.cur.Attr.Mode |= attrReverse - case 21, 22: - t.cur.Attr.Mode &^= attrBold - case 23: - t.cur.Attr.Mode &^= attrItalic - case 24: - t.cur.Attr.Mode &^= attrUnderline - case 25, 26: - t.cur.Attr.Mode &^= attrBlink - case 27: - t.cur.Attr.Mode &^= attrReverse - case 38: - if i+2 < len(attr) && attr[i+1] == 5 { - i += 2 - if between(attr[i], 0, 255) { - t.cur.Attr.FG = Color(attr[i]) - } else { - t.logf("bad fgcolor %d\n", attr[i]) - } - } else if i+4 < len(attr) && attr[i+1] == 2 { - i += 4 - r, g, b := attr[i-2], attr[i-1], attr[i] - if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) { - t.logf("bad fg rgb color (%d,%d,%d)\n", r, g, b) - } else { - t.cur.Attr.FG = Color(r<<16 | g<<8 | b) - } - } else { - t.logf("gfx attr %d unknown\n", a) - } - case 39: - t.cur.Attr.FG = DefaultFG - case 48: - if i+2 < len(attr) && attr[i+1] == 5 { - i += 2 - if between(attr[i], 0, 255) { - t.cur.Attr.BG = Color(attr[i]) - } else { - t.logf("bad bgcolor %d\n", attr[i]) - } - } else if i+4 < len(attr) && attr[i+1] == 2 { - i += 4 - r, g, b := attr[i-2], attr[i-1], attr[i] - if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) { - t.logf("bad bg rgb color (%d,%d,%d)\n", r, g, b) - } else { - t.cur.Attr.BG = Color(r<<16 | g<<8 | b) - } - } else { - t.logf("gfx attr %d unknown\n", a) - } - case 49: - t.cur.Attr.BG = DefaultBG - default: - if between(a, 30, 37) { - t.cur.Attr.FG = Color(a - 30) - } else if between(a, 40, 47) { - t.cur.Attr.BG = Color(a - 40) - } else if between(a, 90, 97) { - t.cur.Attr.FG = Color(a - 90 + 8) - } else if between(a, 100, 107) { - t.cur.Attr.BG = Color(a - 100 + 8) - } else { - t.logf("gfx attr %d unknown\n", a) - } - } - } -} - -func (t *State) insertBlanks(n int) { - src := t.cur.X - dst := src + n - size := t.cols - dst - t.changed |= ChangedScreen - t.dirty[t.cur.Y] = true - - if dst >= t.cols { - t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) - } else { - copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size]) - t.clear(src, t.cur.Y, dst-1, t.cur.Y) - } -} - -func (t *State) insertBlankLines(n int) { - if t.cur.Y < t.top || t.cur.Y > t.bottom { - return - } - t.scrollDown(t.cur.Y, n) -} - -func (t *State) deleteLines(n int) { - if t.cur.Y < t.top || t.cur.Y > t.bottom { - return - } - t.scrollUp(t.cur.Y, n) -} - -func (t *State) deleteChars(n int) { - src := t.cur.X + n - dst := t.cur.X - size := t.cols - src - t.changed |= ChangedScreen - t.dirty[t.cur.Y] = true - - if src >= t.cols { - t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y) - } else { - copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size]) - t.clear(t.cols-n, t.cur.Y, t.cols-1, t.cur.Y) - } -} - -func (t *State) setTitle(title string) { - t.changed |= ChangedTitle - t.title = title -} - -func (t *State) Size() (cols, rows int) { - return t.cols, t.rows -} - -func (t *State) String() string { - t.Lock() - defer t.Unlock() - - var view []rune - for y := 0; y < t.rows; y++ { - for x := 0; x < t.cols; x++ { - attr := t.Cell(x, y) - view = append(view, attr.Char) - } - view = append(view, '\n') - } - - return string(view) -} diff --git a/vendor/github.com/hinshun/vt10x/str.go b/vendor/github.com/hinshun/vt10x/str.go deleted file mode 100644 index 2a42b04f6..000000000 --- a/vendor/github.com/hinshun/vt10x/str.go +++ /dev/null @@ -1,330 +0,0 @@ -package vt10x - -import ( - "fmt" - "math" - "regexp" - "strconv" - "strings" -) - -// STR sequences are similar to CSI sequences, but have string arguments (and -// as far as I can tell, don't really have a name; STR is the name I took from -// suckless which I imagine comes from rxvt or xterm). -type strEscape struct { - typ rune - buf []rune - args []string -} - -func (s *strEscape) reset() { - s.typ = 0 - s.buf = s.buf[:0] - s.args = nil -} - -func (s *strEscape) put(c rune) { - // TODO: improve allocs with an array backed slice; bench first - if len(s.buf) < 256 { - s.buf = append(s.buf, c) - } - // Going by st, it is better to remain silent when the STR sequence is not - // ended so that it is apparent to users something is wrong. The length sanity - // check ensures we don't absorb the entire stream into memory. - // TODO: see what rxvt or xterm does -} - -func (s *strEscape) parse() { - s.args = strings.Split(string(s.buf), ";") -} - -func (s *strEscape) arg(i, def int) int { - if i >= len(s.args) || i < 0 { - return def - } - i, err := strconv.Atoi(s.args[i]) - if err != nil { - return def - } - return i -} - -func (s *strEscape) argString(i int, def string) string { - if i >= len(s.args) || i < 0 { - return def - } - return s.args[i] -} - -func (t *State) handleSTR() { - s := &t.str - s.parse() - - switch s.typ { - case ']': // OSC - operating system command - var p *string - switch d := s.arg(0, 0); d { - case 0, 1, 2: - title := s.argString(1, "") - if title != "" { - t.setTitle(title) - } - case 10: - if len(s.args) < 2 { - break - } - - c := s.argString(1, "") - p := &c - if p != nil && *p == "?" { - t.oscColorResponse(int(DefaultFG), 10) - } else if err := t.setColorName(int(DefaultFG), p); err != nil { - t.logf("invalid foreground color: %s\n", maybe(p)) - } else { - // TODO: redraw - } - case 11: - if len(s.args) < 2 { - break - } - - c := s.argString(1, "") - p := &c - if p != nil && *p == "?" { - t.oscColorResponse(int(DefaultBG), 11) - } else if err := t.setColorName(int(DefaultBG), p); err != nil { - t.logf("invalid cursor color: %s\n", maybe(p)) - } else { - // TODO: redraw - } - // case 12: - // if len(s.args) < 2 { - // break - // } - - // c := s.argString(1, "") - // p := &c - // if p != nil && *p == "?" { - // t.oscColorResponse(int(DefaultCursor), 12) - // } else if err := t.setColorName(int(DefaultCursor), p); err != nil { - // t.logf("invalid background color: %s\n", p) - // } else { - // // TODO: redraw - // } - case 4: // color set - if len(s.args) < 3 { - break - } - - c := s.argString(2, "") - p = &c - fallthrough - case 104: // color reset - j := -1 - if len(s.args) > 1 { - j = s.arg(1, 0) - } - if p != nil && *p == "?" { // report - t.osc4ColorResponse(j) - } else if err := t.setColorName(j, p); err != nil { - if !(d == 104 && len(s.args) <= 1) { - t.logf("invalid color j=%d, p=%s\n", j, maybe(p)) - } - } else { - // TODO: redraw - } - default: - t.logf("unknown OSC command %d\n", d) - // TODO: s.dump() - } - case 'k': // old title set compatibility - title := s.argString(0, "") - if title != "" { - t.setTitle(title) - } - default: - // TODO: Ignore these codes instead of complain? - // 'P': // DSC - device control string - // '_': // APC - application program command - // '^': // PM - privacy message - - t.logf("unhandled STR sequence '%c'\n", s.typ) - // t.str.dump() - } -} - -func (t *State) setColorName(j int, p *string) error { - if !between(j, 0, 1<<24) { - return fmt.Errorf("invalid color value %d", j) - } - - if p == nil { - // restore color - delete(t.colorOverride, Color(j)) - } else { - // set color - r, g, b, err := parseColor(*p) - if err != nil { - return err - } - t.colorOverride[Color(j)] = Color(r<<16 | g<<8 | b) - } - - return nil -} - -func (t *State) oscColorResponse(j, num int) { - if j < 0 { - t.logf("failed to fetch osc color %d\n", j) - return - } - - k, ok := t.colorOverride[Color(j)] - if ok { - j = int(k) - } - - r, g, b := rgb(j) - t.w.Write([]byte(fmt.Sprintf("\033]%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", num, r, r, g, g, b, b))) -} - -func (t *State) osc4ColorResponse(j int) { - if j < 0 { - t.logf("failed to fetch osc4 color %d\n", j) - return - } - - k, ok := t.colorOverride[Color(j)] - if ok { - j = int(k) - } - - r, g, b := rgb(j) - t.w.Write([]byte(fmt.Sprintf("\033]4;%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", j, r, r, g, g, b, b))) -} - -func rgb(j int) (r, g, b int) { - return (j >> 16) & 0xff, (j >> 8) & 0xff, j & 0xff -} - -var ( - RGBPattern = regexp.MustCompile(`^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$`) - HashPattern = regexp.MustCompile(`[\da-f]`) -) - -func parseColor(p string) (r, g, b int, err error) { - if len(p) == 0 { - err = fmt.Errorf("empty color spec") - return - } - - low := strings.ToLower(p) - if strings.HasPrefix(low, "rgb:") { - low = low[4:] - sm := RGBPattern.FindAllStringSubmatch(low, -1) - if len(sm) != 1 || len(sm[0]) == 0 { - err = fmt.Errorf("invalid rgb color spec: %s", p) - return - } - m := sm[0] - - var base float64 - if len(m[1]) > 0 { - base = 15 - } else if len(m[4]) > 0 { - base = 255 - } else if len(m[7]) > 0 { - base = 4095 - } else { - base = 65535 - } - - r64, err := strconv.ParseInt(firstNonEmpty(m[1], m[4], m[7], m[10]), 16, 0) - if err != nil { - return r, g, b, err - } - - g64, err := strconv.ParseInt(firstNonEmpty(m[2], m[5], m[8], m[11]), 16, 0) - if err != nil { - return r, g, b, err - } - - b64, err := strconv.ParseInt(firstNonEmpty(m[3], m[6], m[9], m[12]), 16, 0) - if err != nil { - return r, g, b, err - } - - r = int(math.Round(float64(r64) / base * 255)) - g = int(math.Round(float64(g64) / base * 255)) - b = int(math.Round(float64(b64) / base * 255)) - return r, g, b, nil - } else if strings.HasPrefix(low, "#") { - low = low[1:] - m := HashPattern.FindAllString(low, -1) - if !oneOf(len(m), []int{3, 6, 9, 12}) { - err = fmt.Errorf("invalid hash color spec: %s", p) - return - } - - adv := len(low) / 3 - for i := 0; i < 3; i++ { - c, err := strconv.ParseInt(low[adv*i:adv*i+adv], 16, 0) - if err != nil { - return r, g, b, err - } - - var v int64 - switch adv { - case 1: - v = c << 4 - case 2: - v = c - case 3: - v = c >> 4 - default: - v = c >> 8 - } - - switch i { - case 0: - r = int(v) - case 1: - g = int(v) - case 2: - b = int(v) - } - } - return - } else { - err = fmt.Errorf("invalid color spec: %s", p) - return - } -} - -func maybe(p *string) string { - if p == nil { - return "" - } - return *p -} - -func firstNonEmpty(strs ...string) string { - if len(strs) == 0 { - return "" - } - for _, str := range strs { - if len(str) > 0 { - return str - } - } - return strs[len(strs)-1] -} - -func oneOf(v int, is []int) bool { - for _, i := range is { - if v == i { - return true - } - } - return false -} diff --git a/vendor/github.com/hinshun/vt10x/vt.go b/vendor/github.com/hinshun/vt10x/vt.go deleted file mode 100644 index c4e58dd6a..000000000 --- a/vendor/github.com/hinshun/vt10x/vt.go +++ /dev/null @@ -1,89 +0,0 @@ -package vt10x - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" -) - -// Terminal represents the virtual terminal emulator. -type Terminal interface { - // View displays the virtual terminal. - View - - // Write parses input and writes terminal changes to state. - io.Writer - - // Parse blocks on read on pty or io.Reader, then parses sequences until - // buffer empties. State is locked as soon as first rune is read, and unlocked - // when buffer is empty. - Parse(bf *bufio.Reader) error -} - -// View represents the view of the virtual terminal emulator. -type View interface { - // String dumps the virtual terminal contents. - fmt.Stringer - - // Size returns the size of the virtual terminal. - Size() (cols, rows int) - - // Resize changes the size of the virtual terminal. - Resize(cols, rows int) - - // Mode returns the current terminal mode.// - Mode() ModeFlag - - // Title represents the title of the console window. - Title() string - - // Cell returns the glyph containing the character code, foreground color, and - // background color at position (x, y) relative to the top left of the terminal. - Cell(x, y int) Glyph - - // Cursor returns the current position of the cursor. - Cursor() Cursor - - // CursorVisible returns the visible state of the cursor. - CursorVisible() bool - - // Lock locks the state object's mutex. - Lock() - - // Unlock resets change flags and unlocks the state object's mutex. - Unlock() -} - -type TerminalOption func(*TerminalInfo) - -type TerminalInfo struct { - w io.Writer - cols, rows int -} - -func WithWriter(w io.Writer) TerminalOption { - return func(info *TerminalInfo) { - info.w = w - } -} - -func WithSize(cols, rows int) TerminalOption { - return func(info *TerminalInfo) { - info.cols = cols - info.rows = rows - } -} - -// New returns a new virtual terminal emulator. -func New(opts ...TerminalOption) Terminal { - info := TerminalInfo{ - w: ioutil.Discard, - cols: 80, - rows: 24, - } - for _, opt := range opts { - opt(&info) - } - return newTerminal(info) -} diff --git a/vendor/github.com/hinshun/vt10x/vt_other.go b/vendor/github.com/hinshun/vt10x/vt_other.go deleted file mode 100644 index c9d364ee5..000000000 --- a/vendor/github.com/hinshun/vt10x/vt_other.go +++ /dev/null @@ -1,107 +0,0 @@ -// +build plan9 nacl windows - -package vt10x - -import ( - "bufio" - "bytes" - "io" - "unicode" - "unicode/utf8" -) - -type terminal struct { - *State -} - -func newTerminal(info TerminalInfo) *terminal { - t := &terminal{newState(info.w)} - t.init(info.cols, info.rows) - return t -} - -func (t *terminal) init(cols, rows int) { - t.numlock = true - t.state = t.parse - t.cur.Attr.FG = DefaultFG - t.cur.Attr.BG = DefaultBG - t.Resize(cols, rows) - t.reset() -} - -func (t *terminal) Write(p []byte) (int, error) { - var written int - r := bytes.NewReader(p) - t.lock() - defer t.unlock() - for { - c, sz, err := r.ReadRune() - if err != nil { - if err == io.EOF { - break - } - return written, err - } - written += sz - if c == unicode.ReplacementChar && sz == 1 { - if r.Len() == 0 { - // not enough bytes for a full rune - return written - 1, nil - } - t.logln("invalid utf8 sequence") - continue - } - t.put(c) - } - return written, nil -} - -// TODO: add tests for expected blocking behavior -func (t *terminal) Parse(br *bufio.Reader) error { - var locked bool - defer func() { - if locked { - t.unlock() - } - }() - for { - c, sz, err := br.ReadRune() - if err != nil { - return err - } - if c == unicode.ReplacementChar && sz == 1 { - t.logln("invalid utf8 sequence") - break - } - if !locked { - t.lock() - locked = true - } - - // put rune for parsing and update state - t.put(c) - - // break if our buffer is empty, or if buffer contains an - // incomplete rune. - n := br.Buffered() - if n == 0 || (n < 4 && !fullRuneBuffered(br)) { - break - } - } - return nil -} - -func fullRuneBuffered(br *bufio.Reader) bool { - n := br.Buffered() - buf, err := br.Peek(n) - if err != nil { - return false - } - return utf8.FullRune(buf) -} - -func (t *terminal) Resize(cols, rows int) { - t.lock() - defer t.unlock() - _ = t.resize(cols, rows) -} diff --git a/vendor/github.com/hinshun/vt10x/vt_posix.go b/vendor/github.com/hinshun/vt10x/vt_posix.go deleted file mode 100644 index 80644f4bd..000000000 --- a/vendor/github.com/hinshun/vt10x/vt_posix.go +++ /dev/null @@ -1,108 +0,0 @@ -// +build linux darwin dragonfly solaris openbsd netbsd freebsd - -package vt10x - -import ( - "bufio" - "bytes" - "io" - "unicode" - "unicode/utf8" -) - -type terminal struct { - *State -} - -func newTerminal(info TerminalInfo) *terminal { - t := &terminal{newState(info.w)} - t.init(info.cols, info.rows) - return t -} - -func (t *terminal) init(cols, rows int) { - t.numlock = true - t.state = t.parse - t.cur.Attr.FG = DefaultFG - t.cur.Attr.BG = DefaultBG - t.Resize(cols, rows) - t.reset() -} - -// Write parses input and writes terminal changes to state. -func (t *terminal) Write(p []byte) (int, error) { - var written int - r := bytes.NewReader(p) - t.lock() - defer t.unlock() - for { - c, sz, err := r.ReadRune() - if err != nil { - if err == io.EOF { - break - } - return written, err - } - written += sz - if c == unicode.ReplacementChar && sz == 1 { - if r.Len() == 0 { - // not enough bytes for a full rune - return written - 1, nil - } - t.logln("invalid utf8 sequence") - continue - } - t.put(c) - } - return written, nil -} - -// TODO: add tests for expected blocking behavior -func (t *terminal) Parse(br *bufio.Reader) error { - var locked bool - defer func() { - if locked { - t.unlock() - } - }() - for { - c, sz, err := br.ReadRune() - if err != nil { - return err - } - if c == unicode.ReplacementChar && sz == 1 { - t.logln("invalid utf8 sequence") - break - } - if !locked { - t.lock() - locked = true - } - - // put rune for parsing and update state - t.put(c) - - // break if our buffer is empty, or if buffer contains an - // incomplete rune. - n := br.Buffered() - if n == 0 || (n < 4 && !fullRuneBuffered(br)) { - break - } - } - return nil -} - -func fullRuneBuffered(br *bufio.Reader) bool { - n := br.Buffered() - buf, err := br.Peek(n) - if err != nil { - return false - } - return utf8.FullRune(buf) -} - -func (t *terminal) Resize(cols, rows int) { - t.lock() - defer t.unlock() - _ = t.resize(cols, rows) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 555e8207e..ac2256a94 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -385,7 +385,6 @@ github.com/hashicorp/go-multierror github.com/hashicorp/go-version # github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 ## explicit; go 1.14 -github.com/hinshun/vt10x # github.com/imdario/mergo v0.3.12 ## explicit; go 1.13 github.com/imdario/mergo