bugfix: orbit linux zenity progress windows (#24280)

This commit is contained in:
Tim Lee 2024-12-05 10:02:03 -05:00 committed by GitHub
parent 4f5fb80385
commit 7547dcb74e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 110 additions and 119 deletions

View file

@ -213,13 +213,8 @@ jobs:
# Here we generate the Fleet Desktop and osqueryd targets for
# macOS which can only be generated from a macOS host.
build-macos-targets:
# Set macOS version to '12' (current equivalent to macos-latest) for
# building the binary. This ensures compatibility with macOS version 13 and
# later, avoiding runtime errors on systems using macOS 13 or newer.
#
# Note: Update this version to '13' once GitHub marks macOS 13 as stable
# or if we revise our minimum supported macOS version.
runs-on: macos-12
# Set macOS version to '13' for building the binary as Fleet's minimum supported macOS version.
runs-on: macos-13
steps:
- name: Harden Runner
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0

View file

@ -0,0 +1 @@
* fixed issue where the linux encryption progress window in zenity was not displaying properly

View file

@ -26,7 +26,7 @@ type Dialog interface {
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
ShowProgress(opts ProgressOptions) (cancelFunc func() error, err error)
}
// EntryOptions represents options for a dialog that accepts end user input.

View file

@ -2,6 +2,8 @@
// SYSTEM service on Windows) as the current login user.
package execuser
import "io"
type eopts struct {
env [][2]string
args [][2]string
@ -49,3 +51,11 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er
}
return runWithOutput(path, o)
}
func RunWithStdin(path string, opts ...Option) (io.WriteCloser, error) {
var o eopts
for _, fn := range opts {
fn(&o)
}
return runWithStdin(path, o)
}

View file

@ -54,3 +54,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 runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
return nil, errors.New("not implemented")
}

View file

@ -79,6 +79,35 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
return output, exitCode, nil
}
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
args, err := getUserAndDisplayArgs(path, opts)
if err != nil {
return nil, 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())
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("open path %q: %w", path, err)
}
return stdin, nil
}
func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
user, err := getLoginUID()
if err != nil {

View file

@ -8,6 +8,7 @@ package execuser
import (
"errors"
"fmt"
"io"
"os"
"unsafe"
@ -121,6 +122,10 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
return nil, 0, errors.New("not implemented")
}
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
return nil, errors.New("not implemented")
}
// getCurrentUserSessionId will attempt to resolve
// the session ID of the user currently active on
// the system.

View file

@ -67,7 +67,7 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error {
if err := removeKeySlot(ctx, devicePath, *keyslot); err != nil {
log.Error().Err(err).Msgf("failed to remove key slot %d", *keyslot)
}
return fmt.Errorf("Failed to get salt for key slot: %w", err)
response.Err = fmt.Sprintf("Failed to get salt for key slot: %s", err)
}
response.Salt = salt
}
@ -118,13 +118,18 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
return nil, nil, nil
}
err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
cancelProgress, err := lr.notifier.ShowProgress(dialog.ProgressOptions{
Title: infoTitle,
Text: "Validating passphrase...",
})
if err != nil {
log.Error().Err(err).Msg("failed to show progress dialog")
}
defer func() {
if err := cancelProgress(); err != nil {
log.Debug().Err(err).Msg("failed to cancel progress dialog")
}
}()
// Validate the passphrase
for {
@ -147,23 +152,26 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
return nil, nil, nil
}
err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
Title: infoTitle,
Text: "Validating passphrase...",
})
if err != nil {
log.Error().Err(err).Msg("failed to show progress dialog after retry")
}
}
err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
if err := cancelProgress(); err != nil {
log.Error().Err(err).Msg("failed to cancel progress dialog")
}
cancelProgress, err = lr.notifier.ShowProgress(dialog.ProgressOptions{
Title: infoTitle,
Text: "Key escrow in progress...",
Text: "Escrowing key...",
})
if err != nil {
log.Error().Err(err).Msg("failed to show progress dialog")
}
defer func() {
if err := cancelProgress(); err != nil {
log.Error().Err(err).Msg("failed to cancel progress dialog")
}
}()
escrowPassphrase, err := generateRandomPassphrase()
if err != nil {
return nil, nil, fmt.Errorf("Failed to generate random passphrase: %w", err)

View file

@ -7,9 +7,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/rs/zerolog/log"
)
const zenityProcessName = "zenity"
@ -18,26 +16,21 @@ 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
// killZenityFunc can be set in tests to mock killing the zenity process.
killZenityFunc func()
cmdWithCancel func(args ...string) (func() error, 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,
killZenityFunc: killZenityProcesses,
cmdWithOutput: execCmdWithOutput,
cmdWithCancel: execCmdWithCancel,
}
}
// 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) {
z.killZenityFunc()
args := []string{"--entry"}
if opts.Title != "" {
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
@ -69,8 +62,6 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt
// ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown.
func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
z.killZenityFunc()
args := []string{"--info"}
if opts.Title != "" {
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
@ -95,18 +86,9 @@ func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) 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 {
z.killZenityFunc()
// ShowProgress starts a Zenity pulsating progress dialog with the given options.
// It returns a cancel function that can be used to cancel the dialog.
func (z *Zenity) ShowProgress(opts dialog.ProgressOptions) (func() error, error) {
args := []string{"--progress"}
if opts.Title != "" {
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
@ -121,12 +103,15 @@ func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions)
// --no-cancel disables the cancel button
args = append(args, "--no-cancel")
err := z.cmdWithWait(ctx, args...)
// --auto-close automatically closes the dialog when stdin is closed
args = append(args, "--auto-close")
cancel, err := z.cmdWithCancel(args...)
if err != nil {
return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
return nil, fmt.Errorf("failed to start progress dialog: %w", err)
}
return nil
return cancel, nil
}
func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error) {
@ -143,19 +128,16 @@ func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error)
return output, exitCode, err
}
func execCmdWithWait(ctx context.Context, args ...string) error {
func execCmdWithCancel(args ...string) (func() error, error) {
var opts []execuser.Option
for _, arg := range args {
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
}
_, err := execuser.Run(zenityProcessName, opts...)
return err
}
func killZenityProcesses() {
_, err := platform.KillAllProcessByName(zenityProcessName)
stdin, err := execuser.RunWithStdin(zenityProcessName, opts...)
if err != nil {
log.Warn().Err(err).Msg("failed to kill zenity process")
return nil, err
}
return stdin.Close, err
}

View file

@ -15,7 +15,6 @@ type mockExecCmd struct {
output []byte
exitCode int
capturedArgs []string
waitDuration time.Duration
}
// MockCommandContext simulates exec.CommandContext and captures arguments
@ -29,17 +28,10 @@ func (m *mockExecCmd) runWithOutput(ctx context.Context, args ...string) ([]byte
return m.output, m.exitCode, nil
}
func (m *mockExecCmd) runWithWait(ctx context.Context, args ...string) error {
func (m *mockExecCmd) runWithStdin(args ...string) (func() error, error) {
m.capturedArgs = append(m.capturedArgs, args...)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(m.waitDuration):
}
return nil
return nil, nil
}
func TestShowEntryArgs(t *testing.T) {
@ -76,8 +68,7 @@ func TestShowEntryArgs(t *testing.T) {
output: []byte("some output"),
}
z := &Zenity{
cmdWithOutput: mock.runWithOutput,
killZenityFunc: func() {},
cmdWithOutput: mock.runWithOutput,
}
output, err := z.ShowEntry(ctx, tt.opts)
assert.NoError(t, err)
@ -118,8 +109,7 @@ func TestShowEntryError(t *testing.T) {
exitCode: tt.exitCode,
}
z := &Zenity{
cmdWithOutput: mock.runWithOutput,
killZenityFunc: func() {},
cmdWithOutput: mock.runWithOutput,
}
output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
require.ErrorIs(t, err, tt.expectedErr)
@ -135,8 +125,7 @@ func TestShowEntrySuccess(t *testing.T) {
output: []byte("some output"),
}
z := &Zenity{
cmdWithOutput: mock.runWithOutput,
killZenityFunc: func() {},
cmdWithOutput: mock.runWithOutput,
}
output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
assert.NoError(t, err)
@ -171,8 +160,7 @@ func TestShowInfoArgs(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
mock := &mockExecCmd{}
z := &Zenity{
cmdWithOutput: mock.runWithOutput,
killZenityFunc: func() {},
cmdWithOutput: mock.runWithOutput,
}
err := z.ShowInfo(ctx, tt.opts)
assert.NoError(t, err)
@ -207,8 +195,7 @@ func TestShowInfoError(t *testing.T) {
exitCode: tt.exitCode,
}
z := &Zenity{
cmdWithOutput: mock.runWithOutput,
killZenityFunc: func() {},
cmdWithOutput: mock.runWithOutput,
}
err := z.ShowInfo(ctx, dialog.InfoOptions{})
require.ErrorIs(t, err, tt.expectedErr)
@ -217,8 +204,6 @@ func TestShowInfoError(t *testing.T) {
}
func TestProgressArgs(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
opts dialog.ProgressOptions
@ -230,7 +215,7 @@ func TestProgressArgs(t *testing.T) {
Title: "A Title",
Text: "Some text",
},
expectedArgs: []string{"--progress", "--title=A Title", "--text=Some text", "--pulsate", "--no-cancel"},
expectedArgs: []string{"--progress", "--title=A Title", "--text=Some text", "--pulsate", "--no-cancel", "--auto-close"},
},
}
@ -238,38 +223,11 @@ func TestProgressArgs(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
mock := &mockExecCmd{}
z := &Zenity{
cmdWithWait: mock.runWithWait,
killZenityFunc: func() {},
cmdWithCancel: mock.runWithStdin,
}
err := z.ShowProgress(ctx, tt.opts)
_, err := z.ShowProgress(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,
killZenityFunc: func() {},
}
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)
}

View file

@ -27,21 +27,20 @@ func main() {
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)
}
}()
cancelProgress, err := prompt.ShowProgress(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()
if err := cancelProgress(); err != nil {
fmt.Println("Err cancelProgress")
panic(err)
}
err = prompt.ShowInfo(ctx, dialog.InfoOptions{
Title: "Zenity Test Info Title",