Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cli] Reimplement the interactive renderer #11200

Merged
merged 4 commits into from Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,4 @@
changes:
- type: feat
scope: cli/display
description: Improve the usability of the interactive dipslay by making the treetable scrollable
75 changes: 55 additions & 20 deletions pkg/backend/display/jsonmessage.go
Expand Up @@ -23,7 +23,6 @@ import (
"os"
"unicode/utf8"

gotty "github.com/ijc/Gotty"
"golang.org/x/crypto/ssh/terminal"

"github.com/pulumi/pulumi/pkg/v3/engine"
Expand Down Expand Up @@ -140,14 +139,57 @@ type messageRenderer struct {
nonInteractiveSpinner cmdutil.Spinner

progressOutput chan<- Progress
closed <-chan bool

// Cache of lines we've already printed. We don't print a progress message again if it hasn't
// changed between the last time we printed and now.
printedProgressCache map[string]Progress
}

func newInteractiveMessageRenderer(stdout io.Writer, opts Options,
terminalWidth, terminalHeight int, termInfo termInfo) progressRenderer {

progressOutput, closed := make(chan Progress), make(chan bool)
go func() {
ShowProgressOutput(progressOutput, stdout, termInfo)
close(closed)
}()

return &messageRenderer{
opts: opts,
isTerminal: true,
terminalWidth: terminalWidth,
terminalHeight: terminalHeight,
progressOutput: progressOutput,
closed: closed,
printedProgressCache: make(map[string]Progress),
}
}

func newNonInteractiveRenderer(stdout io.Writer, op string, opts Options) progressRenderer {
progressOutput, closed := make(chan Progress), make(chan bool)
go func() {
ShowProgressOutput(progressOutput, stdout, nil)
close(closed)
}()

spinner, ticker := cmdutil.NewSpinnerAndTicker(
fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op),
nil, opts.Color, 1 /*timesPerSecond*/)
ticker.Stop()

return &messageRenderer{
opts: opts,
progressOutput: progressOutput,
closed: closed,
printedProgressCache: make(map[string]Progress),
nonInteractiveSpinner: spinner,
}
}

func (r *messageRenderer) Close() error {
close(r.progressOutput)
<-r.closed
return nil
}

Expand Down Expand Up @@ -196,7 +238,7 @@ func (r *messageRenderer) println(display *ProgressDisplay, line string) {

func (r *messageRenderer) tick(display *ProgressDisplay) {
if r.isTerminal {
r.render(display)
r.render(display, false)
} else {
// Update the spinner to let the user know that that work is still happening.
r.nonInteractiveSpinner.Tick()
Expand Down Expand Up @@ -233,7 +275,7 @@ func (r *messageRenderer) renderRow(display *ProgressDisplay,
func (r *messageRenderer) rowUpdated(display *ProgressDisplay, row Row) {
if r.isTerminal {
// if we're in a terminal, then refresh everything so that all our columns line up
r.render(display)
r.render(display, false)
} else {
// otherwise, just print out this single row.
colorizedColumns := row.ColorizedColumns()
Expand All @@ -246,17 +288,20 @@ func (r *messageRenderer) systemMessage(display *ProgressDisplay, payload engine
if r.isTerminal {
// if we're in a terminal, then refresh everything. The system events will come after
// all the normal rows
r.render(display)
r.render(display, false)
} else {
// otherwise, in a non-terminal, just print out the actual event.
r.writeSimpleMessage(renderStdoutColorEvent(payload, display.opts))
}
}

func (r *messageRenderer) done(display *ProgressDisplay) {
if r.isTerminal {
r.render(display, false)
}
}

func (r *messageRenderer) render(display *ProgressDisplay) {
func (r *messageRenderer) render(display *ProgressDisplay, done bool) {
if !r.isTerminal || display.headerRow == nil {
return
}
Expand Down Expand Up @@ -316,6 +361,10 @@ func (r *messageRenderer) render(display *ProgressDisplay) {
systemID++
}
}

if done {
r.println(display, "")
}
}

// Ensure our stored dimension info is up to date.
Expand All @@ -336,25 +385,11 @@ func (r *messageRenderer) updateTerminalDimensions() {
// ShowProgressOutput displays a progress stream from `in` to `out`, `isTerminal` describes if
// `out` is a terminal. If this is the case, it will print `\n` at the end of each line and move the
// cursor while displaying.
func ShowProgressOutput(in <-chan Progress, out io.Writer, isTerminal bool) {
func ShowProgressOutput(in <-chan Progress, out io.Writer, info termInfo) {
var (
ids = make(map[string]int)
)

var info termInfo

if isTerminal {
term := os.Getenv("TERM")
if term == "" {
term = "vt102"
}

var err error
if info, err = gotty.OpenTermInfo(term); err != nil {
info = &noTermInfo{}
}
}

for jm := range in {
diff := 0

Expand Down
1 change: 1 addition & 0 deletions pkg/backend/display/options.go
Expand Up @@ -50,6 +50,7 @@ type Options struct {
JSONDisplay bool // true if we should emit the entire diff as JSON.
EventLogPath string // the path to the file to use for logging events, if any.
Debug bool // true to enable debug output.
Stdin io.Reader // the reader to use for stdin. Defaults to os.Stdin if unset.
Stdout io.Writer // the writer to use for stdout. Defaults to os.Stdout if unset.
Stderr io.Writer // the writer to use for stderr. Defaults to os.Stderr if unset.
SuppressTimings bool // true to suppress displaying timings of resource actions
Expand Down
79 changes: 14 additions & 65 deletions pkg/backend/display/progress.go
Expand Up @@ -19,17 +19,13 @@ import (
"bytes"
"fmt"
"io"
"math"
"os"
"sort"
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/moby/term"
"golang.org/x/crypto/ssh/terminal"

"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
Expand Down Expand Up @@ -228,45 +224,26 @@ func getEventUrnAndMetadata(event engine.Event) (resource.URN, *engine.StepEvent
func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.Name, proj tokens.PackageName,
events <-chan engine.Event, done chan<- bool, opts Options, isPreview bool) {

stdin := opts.Stdin
if stdin == nil {
stdin = os.Stdin
}
stdout := opts.Stdout
if stdout == nil {
stdout = os.Stdout
}
stderr := opts.Stderr
if stderr == nil {
stderr = os.Stderr
}

// Create a ticker that will update all our status messages once a second. Any
// in-flight resources will get a varying . .. ... ticker appended to them to
// let the user know what is still being worked on.
var spinner cmdutil.Spinner
var ticker *time.Ticker
if stdout == os.Stdout && stderr == os.Stderr {
spinner, ticker = cmdutil.NewSpinnerAndTicker(
fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op),
nil, opts.Color, 1 /*timesPerSecond*/)
} else {
spinner = &nopSpinner{}
ticker = time.NewTicker(math.MaxInt64)
}

// The channel we push progress messages into, and which ShowProgressOutput pulls
// from to display to the console.
progressOutput := make(chan Progress)

opStopwatch := newOpStopwatch()

renderer := &messageRenderer{
opts: opts,
progressOutput: progressOutput,
printedProgressCache: make(map[string]Progress),
nonInteractiveSpinner: spinner,
isTerminal := true
renderer, err := newInteractiveRenderer(stdin, stdout, opts)
if err != nil {
fmt.Println(err)
isTerminal, renderer = false, newNonInteractiveRenderer(stdout, op, opts)
}

display := &ProgressDisplay{
action: action,
isPreview: isPreview,
isTerminal: isTerminal,
opts: opts,
renderer: renderer,
stack: stack,
Expand All @@ -277,39 +254,12 @@ func ShowProgressEvents(op string, action apitype.UpdateKind, stack tokens.Name,
urnToID: make(map[resource.URN]string),
colorizedToUncolorized: make(map[string]string),
displayOrderCounter: 1,
opStopwatch: opStopwatch,
opStopwatch: newOpStopwatch(),
}

// Assume we are not displaying in a terminal by default.
renderer.isTerminal = false
if stdout == os.Stdout {
terminalWidth, terminalHeight, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
// If the terminal has a size, use it.
renderer.isTerminal = opts.IsInteractive
renderer.terminalWidth = terminalWidth
renderer.terminalHeight = terminalHeight

// Don't bother attempting to treat this display as a terminal if it has no width/height.
if renderer.isTerminal && (renderer.terminalWidth == 0 || renderer.terminalHeight == 0) {
renderer.isTerminal = false
_, err = fmt.Fprintln(stderr, "Treating display as non-terminal due to 0 width/height.")
contract.IgnoreError(err)
}

// Fetch the canonical stdout stream, configured appropriately.
_, stdout, _ = term.StdStreams()
}
}
display.isTerminal = renderer.isTerminal

go func() {
display.processEvents(ticker, events)
contract.IgnoreClose(display.renderer)
}()

ShowProgressOutput(progressOutput, stdout, display.isTerminal)

ticker := time.NewTicker(1 * time.Second)
display.processEvents(ticker, events)
contract.IgnoreClose(display.renderer)
ticker.Stop()

// let our caller know we're done.
Expand Down Expand Up @@ -410,7 +360,6 @@ func (display *ProgressDisplay) generateTreeNodes() []*treeNode {
}

func (display *ProgressDisplay) addIndentations(treeNodes []*treeNode, isRoot bool, indentation string) {

childIndentation := indentation + "│ "
lastChildIndentation := indentation + " "

Expand Down
28 changes: 28 additions & 0 deletions pkg/backend/display/sigint_unix.go
@@ -0,0 +1,28 @@
// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !windows

package display

import (
"syscall"

"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

func sigint() {
err := syscall.Kill(syscall.Getpid(), syscall.SIGINT)
contract.IgnoreError(err)
}
44 changes: 44 additions & 0 deletions pkg/backend/display/sigint_windows.go
@@ -0,0 +1,44 @@
// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows

package display

import (
"fmt"
"os"
"syscall"

"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

var generateConsoleCtrlEvent *syscall.Proc

func init() {
dll, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
panic(fmt.Errorf("loading kernel32.dll: %w", err))
}
proc, err := dll.FindProc("GenerateConsoleCtrlEvent")
if err != nil {
panic(fmt.Errorf("finding GenerateConsoleCtrlEvent: %w", err))
}
generateConsoleCtrlEvent = proc
}

func sigint() {
_, _, err := generateConsoleCtrlEvent.Call(syscall.CTRL_BREAK_EVENT, uintptr(os.Getpid()))
contract.IgnoreError(err)
}