Skip to content

Commit

Permalink
add progress dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
mostlikelee committed Nov 19, 2024
1 parent 37d03e3 commit 2753aa1
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 86 deletions.
23 changes: 20 additions & 3 deletions orbit/pkg/dialog/dialog.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package dialog

// Dialog represents a UI dialog that can be displayed to the end user
// on a host

import (
"context"
"errors"
Expand All @@ -18,13 +15,18 @@ var (
ErrUnknown = errors.New("unknown error")
)

// Dialog represents a UI dialog that can be displayed to the end user
// on a host
type Dialog interface {
// ShowEntry displays a dialog that accepts end user input. It returns the entered
// text or errors ErrCanceled, ErrTimeout, or ErrUnknown.
ShowEntry(ctx context.Context, opts EntryOptions) ([]byte, error)
// ShowInfo displays a dialog that displays information. It returns an error if the dialog
// could not be displayed.
ShowInfo(ctx context.Context, opts InfoOptions) error
// Progress displays a dialog that shows progress. It returns a channel that can be used to
// end the dialog.
ShowProgress(ctx context.Context, opts ProgressOptions) chan struct{}
}

// EntryOptions represents options for a dialog that accepts end user input.
Expand Down Expand Up @@ -53,3 +55,18 @@ type InfoOptions struct {
// Timeout sets the time in seconds before the dialog is automatically closed.
TimeOut time.Duration
}

// ProgressOptions represents options for a dialog that shows progress.
type ProgressOptions struct {
// Title sets the title of the dialog.
Title string

// Text sets the text of the dialog.
Text string

// Pulsate sets the progress bar to pulsate.
Pulsate bool

// NoCancel sets the dialog to grey out the cancel button.
NoCancel bool
}
13 changes: 13 additions & 0 deletions orbit/pkg/execuser/execuser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SYSTEM service on Windows) as the current login user.
package execuser

import "context"

type eopts struct {
env [][2]string
args [][2]string
Expand Down Expand Up @@ -48,3 +50,14 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er
}
return runWithOutput(path, o)
}

// RunWithWait runs an application as the current login user and waits for it to finish
// or to be canceled by the context.
// It assumes the caller is running with high privileges (root on UNIX).
func RunWithWait(ctx context.Context, path string, opts ...Option) error {
var o eopts
for _, fn := range opts {
fn(&o)
}
return runWithWait(ctx, path, o)
}
5 changes: 5 additions & 0 deletions orbit/pkg/execuser/execuser_darwin.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package execuser

import (
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -52,3 +53,7 @@ func run(path string, opts eopts) (lastLogs string, err error) {
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
return nil, 0, errors.New("not implemented")
}

func runWithWait(ctx context.Context, path string, opts eopts) error {
return errors.New("not implemented")
}
126 changes: 55 additions & 71 deletions orbit/pkg/execuser/execuser_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package execuser
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
Expand All @@ -18,56 +19,12 @@ import (

// run uses sudo to run the given path as login user.
func run(path string, opts eopts) (lastLogs string, err error) {
user, err := getLoginUID()
args, err := getUserAndDisplayArgs(path, opts)
if err != nil {
return "", fmt.Errorf("get user: %w", err)
return "", fmt.Errorf("get args: %w", err)
}

// TODO(lucas): Default to display :0 if user DISPLAY environment variable
// could not be found, revisit when working on multi-user/multi-session support.
// This assumes there's only one desktop session and belongs to the
// user returned in `getLoginUID'.
defaultDisplay := ":0"

log.Info().
Str("user", user.name).
Int64("id", user.id).
Msg("attempting to get user's DISPLAY")

display, err := getUserDisplay(user.name, opts)
if err != nil {
log.Error().
Str("user", user.name).
Int64("id", user.id).
Err(err).
Msgf("failed to get user's DISPLAY, using default %s", defaultDisplay)
display = defaultDisplay
} else if display == "" {
log.Warn().
Str("user", user.name).
Int64("id", user.id).
Msgf("user's DISPLAY not found, using default %s", defaultDisplay)
display = defaultDisplay
}

log.Info().
Str("path", path).
Str("user", user.name).
Int64("id", user.id).
Str("display", display).
Msg("running sudo")

args := argsForSudo(user, opts)

args = append(args,
"DISPLAY="+display,
// DBUS_SESSION_BUS_ADDRESS sets the location of the user login session bus.
// Required by the libayatana-appindicator3 library to display a tray icon
// on the desktop session.
//
// This is required for Ubuntu 18, and not required for Ubuntu 21/22
// (because it's already part of the user).
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id),
// Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH.
//
// Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to
Expand All @@ -89,9 +46,59 @@ func run(path string, opts eopts) (lastLogs string, err error) {

// run uses sudo to run the given path as login user and waits for the process to finish.
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
args, err := getUserAndDisplayArgs(path, opts)
if err != nil {
return nil, 0, fmt.Errorf("get args: %w", err)
}

if len(opts.args) > 0 {
for _, arg := range opts.args {
args = append(args, arg[0], arg[1])
}
}

args = append(args, path)

cmd := exec.Command("sudo", args...)
log.Printf("cmd=%s", cmd.String())

output, err = cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
}
return output, exitCode, fmt.Errorf("%q error: %w", path, err)
}

return output, exitCode, nil
}

func runWithWait(ctx context.Context, path string, opts eopts) error {
args, err := getUserAndDisplayArgs(path, opts)
if err != nil {
return fmt.Errorf("get args: %w", err)
}

args = append(args, path)

cmd := exec.CommandContext(ctx, "sudo", args...)
log.Printf("cmd=%s", cmd.String())

if err := cmd.Start(); err != nil {
return fmt.Errorf("cmd %q: %w", path, err)
}

if err := cmd.Wait(); err != nil {
return fmt.Errorf("cmd %q: %w", path, err)
}

return nil
}

func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
user, err := getLoginUID()
if err != nil {
return output, exitCode, fmt.Errorf("get user: %w", err)
return nil, fmt.Errorf("get user: %w", err)
}

// TODO(lucas): Default to display :0 if user DISPLAY environment variable
Expand Down Expand Up @@ -139,32 +146,9 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
// This is required for Ubuntu 18, and not required for Ubuntu 21/22
// (because it's already part of the user).
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id),
// Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH.
//
// Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to
// keep this to support older versions of Fleet Desktop.
fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")),
path,
)

if len(opts.args) > 0 {
for _, arg := range opts.args {
args = append(args, arg[0], arg[1])
}
}

cmd := exec.Command("sudo", args...)
log.Printf("cmd=%s", cmd.String())

output, err = cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
}
return output, exitCode, fmt.Errorf("%q error: %w", path, err)
}

return output, exitCode, nil
return args, nil
}

type user struct {
Expand Down
5 changes: 5 additions & 0 deletions orbit/pkg/execuser/execuser_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package execuser
// To view what was modified/added, you can use the execuser_windows_diff.sh script.

import (
"context"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -121,6 +122,10 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
return nil, 0, errors.New("not implemented")
}

func runWithWait(ctx context.Context, path string, opts eopts) error {
return errors.New("not implemented")
}

// getCurrentUserSessionId will attempt to resolve
// the session ID of the user currently active on
// the system.
Expand Down
49 changes: 43 additions & 6 deletions orbit/pkg/zenity/zenity.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import (
)

type Zenity struct {
// execCmdFn can be set in tests to mock execution of the dialog.
execCmdFn func(ctx context.Context, args ...string) ([]byte, int, error)
// cmdWithOutput can be set in tests to mock execution of the dialog.
cmdWithOutput func(ctx context.Context, args ...string) ([]byte, int, error)
// cmdWithWait can be set in tests to mock execution of the dialog.
cmdWithWait func(ctx context.Context, args ...string) error
}

// NewZenity creates a new Zenity dialog instance for zenity v4 on Linux.
// Zenity implements the Dialog interface.
func New() *Zenity {
return &Zenity{
execCmdFn: execCmd,
cmdWithOutput: execCmdWithOutput,
cmdWithWait: execCmdWithWait,
}
}

Expand All @@ -40,7 +43,7 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt
args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds())))
}

output, statusCode, err := z.execCmdFn(ctx, args...)
output, statusCode, err := z.cmdWithOutput(ctx, args...)
if err != nil {
switch statusCode {
case 1:
Expand Down Expand Up @@ -68,7 +71,7 @@ func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds())))
}

_, statusCode, err := z.execCmdFn(ctx, args...)
_, statusCode, err := z.cmdWithOutput(ctx, args...)
if err != nil {
switch statusCode {
case 5:
Expand All @@ -81,7 +84,32 @@ func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
return nil
}

func execCmd(ctx context.Context, args ...string) ([]byte, int, error) {
// ShowProgress displays a progress dialog. It returns a channel that can be used to
// end the dialog.
func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions) error {
args := []string{"--progress"}
if opts.Title != "" {
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
}
if opts.Text != "" {
args = append(args, fmt.Sprintf("--text=%s", opts.Text))
}
if opts.Pulsate {
args = append(args, "--pulsate")
}
if opts.NoCancel {
args = append(args, "--no-cancel")
}

err := z.cmdWithWait(ctx, args...)
if err != nil {
return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
}

return nil
}

func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error) {
var opts []execuser.Option
for _, arg := range args {
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
Expand All @@ -94,3 +122,12 @@ func execCmd(ctx context.Context, args ...string) ([]byte, int, error) {

return output, exitCode, err
}

func execCmdWithWait(ctx context.Context, args ...string) error {
var opts []execuser.Option
for _, arg := range args {
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
}

return execuser.RunWithWait(ctx, "zenity", opts...)
}
Loading

0 comments on commit 2753aa1

Please sign in to comment.