zenity package for Linux (#23619)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCanceled is returned when the dialog is canceled by the cancel button.
|
||||
ErrCanceled = errors.New("dialog canceled")
|
||||
// ErrTimeout is returned when the dialog is automatically closed due to a timeout.
|
||||
ErrTimeout = errors.New("dialog timed out")
|
||||
// ErrUnknown is returned when an unknown error occurs.
|
||||
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 waits until the
|
||||
// context is cancelled.
|
||||
ShowProgress(ctx context.Context, opts ProgressOptions) error
|
||||
}
|
||||
|
||||
// EntryOptions represents options for a dialog that accepts end user input.
|
||||
type EntryOptions struct {
|
||||
// Title sets the title of the dialog.
|
||||
Title string
|
||||
|
||||
// Text sets the text of the dialog.
|
||||
Text string
|
||||
|
||||
// HideText hides the text entered by the user.
|
||||
HideText bool
|
||||
|
||||
// TimeOut sets the time in seconds before the dialog is automatically closed.
|
||||
TimeOut time.Duration
|
||||
}
|
||||
|
||||
// InfoOptions represents options for a dialog that displays information.
|
||||
type InfoOptions struct {
|
||||
// Title sets the title of the dialog.
|
||||
Title string
|
||||
|
||||
// Text sets the text of the dialog.
|
||||
Text string
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -19,10 +21,6 @@ func WithEnv(name, value string) Option {
|
||||
}
|
||||
|
||||
// WithArg sets command line arguments for the application.
|
||||
//
|
||||
// TODO: for now CLI arguments are only used by the darwin
|
||||
// implementation, just because it's the only platform that needs
|
||||
// them.
|
||||
func WithArg(name, value string) Option {
|
||||
return func(a *eopts) {
|
||||
a.args = append(a.args, [2]string{name, value})
|
||||
@@ -40,3 +38,27 @@ func Run(path string, opts ...Option) (lastLogs string, err error) {
|
||||
}
|
||||
return run(path, o)
|
||||
}
|
||||
|
||||
// RunWithOutput runs an application as the current login user and returns its output.
|
||||
// It assumes the caller is running with high privileges (root on UNIX).
|
||||
//
|
||||
// It blocks until the child process exits.
|
||||
// Non ExitError errors return with a -1 exitCode.
|
||||
func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, err error) {
|
||||
var o eopts
|
||||
for _, fn := range opts {
|
||||
fn(&o)
|
||||
}
|
||||
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. Canceling the context will not return an error.
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package execuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -47,3 +49,11 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
||||
}
|
||||
return tw.String(), nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package execuser
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -18,9 +19,96 @@ import (
|
||||
|
||||
// run uses sudo to run the given path as login user.
|
||||
func run(path string, opts eopts) (lastLogs string, err error) {
|
||||
args, err := getUserAndDisplayArgs(path, opts)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get args: %w", err)
|
||||
}
|
||||
|
||||
args = append(args,
|
||||
// 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,
|
||||
)
|
||||
|
||||
cmd := exec.Command("sudo", args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
log.Printf("cmd=%s", cmd.String())
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("open path %q: %w", path, err)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 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, -1, fmt.Errorf("get args: %w", err)
|
||||
}
|
||||
|
||||
args = append(args, 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 exited with code %d: %w", path, exitCode, err)
|
||||
}
|
||||
return output, -1, 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)
|
||||
|
||||
if len(opts.args) > 0 {
|
||||
for _, arg := range opts.args {
|
||||
args = append(args, arg[0], arg[1])
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", args...)
|
||||
log.Printf("cmd=%s", cmd.String())
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("cmd start %q: %w", path, err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cmd wait %q: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
user, err := getLoginUID()
|
||||
if err != nil {
|
||||
return "", 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
|
||||
@@ -68,23 +156,9 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
||||
// 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,
|
||||
)
|
||||
|
||||
cmd := exec.Command("sudo", args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
log.Printf("cmd=%s", cmd.String())
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("open path %q: %w", path, err)
|
||||
}
|
||||
return "", nil
|
||||
return args, nil
|
||||
}
|
||||
|
||||
type user struct {
|
||||
|
||||
@@ -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"
|
||||
@@ -117,6 +118,14 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
||||
return "", startProcessAsCurrentUser(path, "", "")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// getCurrentUserSessionId will attempt to resolve
|
||||
// the session ID of the user currently active on
|
||||
// the system.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package zenity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
)
|
||||
|
||||
type Zenity struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
// New creates a new Zenity dialog instance for zenity v4 on Linux.
|
||||
// Zenity implements the Dialog interface.
|
||||
func New() *Zenity {
|
||||
return &Zenity{
|
||||
cmdWithOutput: execCmdWithOutput,
|
||||
cmdWithWait: execCmdWithWait,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowEntry displays an dialog that accepts end user input. It returns the entered
|
||||
// text or errors ErrCanceled, ErrTimeout, or ErrUnknown.
|
||||
func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byte, error) {
|
||||
args := []string{"--entry"}
|
||||
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.HideText {
|
||||
args = append(args, "--hide-text")
|
||||
}
|
||||
if opts.TimeOut > 0 {
|
||||
args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds())))
|
||||
}
|
||||
|
||||
output, statusCode, err := z.cmdWithOutput(ctx, args...)
|
||||
if err != nil {
|
||||
switch statusCode {
|
||||
case 1:
|
||||
return nil, ctxerr.Wrap(ctx, dialog.ErrCanceled)
|
||||
case 5:
|
||||
return nil, ctxerr.Wrap(ctx, dialog.ErrTimeout)
|
||||
default:
|
||||
return nil, ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown.
|
||||
func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
|
||||
args := []string{"--info"}
|
||||
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.TimeOut > 0 {
|
||||
args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds())))
|
||||
}
|
||||
|
||||
_, statusCode, err := z.cmdWithOutput(ctx, args...)
|
||||
if err != nil {
|
||||
switch statusCode {
|
||||
case 5:
|
||||
return ctxerr.Wrap(ctx, dialog.ErrTimeout)
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowProgress starts a Zenity progress dialog with the given options.
|
||||
// This function is designed to block until the provided context is canceled.
|
||||
// It is intended to be used within a separate goroutine to avoid blocking
|
||||
// the main execution flow.
|
||||
//
|
||||
// If the context is already canceled, the function will return immediately.
|
||||
//
|
||||
// Use this function for cases where a progress dialog is needed to run
|
||||
// alongside other operations, with explicit cancellation or termination.
|
||||
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))
|
||||
}
|
||||
|
||||
// --pulsate shows a pulsating progress bar
|
||||
args = append(args, "--pulsate")
|
||||
|
||||
// --no-cancel disables the cancel button
|
||||
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
|
||||
}
|
||||
|
||||
output, exitCode, err := execuser.RunWithOutput("zenity", opts...)
|
||||
|
||||
// Trim the newline from zenity output
|
||||
output = bytes.TrimSuffix(output, []byte("\n"))
|
||||
|
||||
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...)
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package zenity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tj/assert"
|
||||
)
|
||||
|
||||
type mockExecCmd struct {
|
||||
output []byte
|
||||
exitCode int
|
||||
capturedArgs []string
|
||||
waitDuration time.Duration
|
||||
}
|
||||
|
||||
// MockCommandContext simulates exec.CommandContext and captures arguments
|
||||
func (m *mockExecCmd) runWithOutput(ctx context.Context, args ...string) ([]byte, int, error) {
|
||||
m.capturedArgs = append(m.capturedArgs, args...)
|
||||
|
||||
if m.exitCode != 0 {
|
||||
return nil, m.exitCode, &exec.ExitError{}
|
||||
}
|
||||
|
||||
return m.output, m.exitCode, nil
|
||||
}
|
||||
|
||||
func (m *mockExecCmd) runWithWait(ctx context.Context, args ...string) error {
|
||||
m.capturedArgs = append(m.capturedArgs, args...)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(m.waitDuration):
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestShowEntryArgs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts dialog.EntryOptions
|
||||
expectedArgs []string
|
||||
}{
|
||||
{
|
||||
name: "Basic Entry",
|
||||
opts: dialog.EntryOptions{
|
||||
Title: "A Title",
|
||||
Text: "Some text",
|
||||
},
|
||||
expectedArgs: []string{"--entry", "--title=A Title", "--text=Some text"},
|
||||
},
|
||||
{
|
||||
name: "All Options",
|
||||
opts: dialog.EntryOptions{
|
||||
Title: "Another Title",
|
||||
Text: "Some more text",
|
||||
HideText: true,
|
||||
TimeOut: 1 * time.Minute,
|
||||
},
|
||||
expectedArgs: []string{"--entry", "--title=Another Title", "--text=Some more text", "--hide-text", "--timeout=60"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{
|
||||
output: []byte("some output"),
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
}
|
||||
output, err := z.ShowEntry(ctx, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedArgs, mock.capturedArgs)
|
||||
assert.Equal(t, []byte("some output"), output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowEntryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
exitCode int
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Dialog Cancelled",
|
||||
exitCode: 1,
|
||||
expectedErr: dialog.ErrCanceled,
|
||||
},
|
||||
{
|
||||
name: "Dialog Timed Out",
|
||||
exitCode: 5,
|
||||
expectedErr: dialog.ErrTimeout,
|
||||
},
|
||||
{
|
||||
name: "Unknown Error",
|
||||
exitCode: 99,
|
||||
expectedErr: dialog.ErrUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{
|
||||
exitCode: tt.exitCode,
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
}
|
||||
output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
|
||||
require.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Nil(t, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowEntrySuccess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock := &mockExecCmd{
|
||||
output: []byte("some output"),
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
}
|
||||
output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("some output"), output)
|
||||
}
|
||||
|
||||
func TestShowInfoArgs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts dialog.InfoOptions
|
||||
expectedArgs []string
|
||||
}{
|
||||
{
|
||||
name: "Basic Entry",
|
||||
opts: dialog.InfoOptions{},
|
||||
expectedArgs: []string{"--info"},
|
||||
},
|
||||
{
|
||||
name: "All Options",
|
||||
opts: dialog.InfoOptions{
|
||||
Title: "Another Title",
|
||||
Text: "Some more text",
|
||||
TimeOut: 1 * time.Minute,
|
||||
},
|
||||
expectedArgs: []string{"--info", "--title=Another Title", "--text=Some more text", "--timeout=60"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
}
|
||||
err := z.ShowInfo(ctx, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedArgs, mock.capturedArgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowInfoError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
exitCode int
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Dialog Timed Out",
|
||||
exitCode: 5,
|
||||
expectedErr: dialog.ErrTimeout,
|
||||
},
|
||||
{
|
||||
name: "Unknown Error",
|
||||
exitCode: 99,
|
||||
expectedErr: dialog.ErrUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{
|
||||
exitCode: tt.exitCode,
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
}
|
||||
err := z.ShowInfo(ctx, dialog.InfoOptions{})
|
||||
require.ErrorIs(t, err, tt.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressArgs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts dialog.ProgressOptions
|
||||
expectedArgs []string
|
||||
}{
|
||||
{
|
||||
name: "Basic Entry",
|
||||
opts: dialog.ProgressOptions{
|
||||
Title: "A Title",
|
||||
Text: "Some text",
|
||||
},
|
||||
expectedArgs: []string{"--progress", "--title=A Title", "--text=Some text", "--pulsate", "--no-cancel"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{}
|
||||
z := &Zenity{
|
||||
cmdWithWait: mock.runWithWait,
|
||||
}
|
||||
err := z.ShowProgress(ctx, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedArgs, mock.capturedArgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressKillOnCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
mock := &mockExecCmd{
|
||||
waitDuration: 5 * time.Second,
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithWait: mock.runWithWait,
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
start := time.Now()
|
||||
|
||||
go func() {
|
||||
_ = z.ShowProgress(ctx, dialog.ProgressOptions{})
|
||||
close(done)
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
assert.True(t, time.Since(start) < 5*time.Second)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
// This is a tool to test the zenity package on Linux
|
||||
// It will show an entry dialog, a progress dialog, and an info dialog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/zenity"
|
||||
)
|
||||
|
||||
func main() {
|
||||
prompt := zenity.New()
|
||||
ctx := context.Background()
|
||||
|
||||
output, err := prompt.ShowEntry(ctx, dialog.EntryOptions{
|
||||
Title: "Zenity Test Entry Title",
|
||||
Text: "Zenity Test Entry Text",
|
||||
HideText: true,
|
||||
TimeOut: 10 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Err ShowEntry")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx, cancelProgress := context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
err := prompt.ShowProgress(ctx, dialog.ProgressOptions{
|
||||
Title: "Zenity Test Progress Title",
|
||||
Text: "Zenity Test Progress Text",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Err ShowProgress")
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
cancelProgress()
|
||||
|
||||
err = prompt.ShowInfo(ctx, dialog.InfoOptions{
|
||||
Title: "Zenity Test Info Title",
|
||||
Text: "Result: " + string(output),
|
||||
TimeOut: 10 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Err ShowInfo")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user