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

add menu update change detection logs #1664

Merged
merged 2 commits into from
Mar 28, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions ee/desktop/runner/menu_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package runner

import (
"encoding/json"
"fmt"
"sync"
)

// menuItemCache is used to keep track of the last set of items
// seen in the menu bar json, to report any changes detected
type menuItemCache struct {
mu sync.RWMutex
cachedItems map[string]struct{}
}

// menuChangeSet is a simple struct used for reporting changes
type menuChangeSet struct {
Old string `json:"old_label"`
New string `json:"new_label"`
}

func newMenuItemCache() *menuItemCache {
return &menuItemCache{
cachedItems: make(map[string]struct{}),
}
}

func (m *menuItemCache) recordMenuUpdates(menuData []byte) ([]menuChangeSet, error) {
m.mu.Lock()
defer m.mu.Unlock()

menuChanges := make([]menuChangeSet, 0)

parsedMenu := struct {
Items []struct {
Label string `json:"label"`
} `json:"items"`
}{}

if err := json.Unmarshal(menuData, &parsedMenu); err != nil {
return menuChanges, fmt.Errorf("unable to parse menu json for change detection: %w", err)
}

newCachedMenuData := make(map[string]struct{})
// first iterate the new menu items, noting any that are new
for _, item := range parsedMenu.Items {
if item.Label == "" { // skip separator sections
continue
}

newCachedMenuData[item.Label] = struct{}{}

if _, ok := m.cachedItems[item.Label]; ok {
continue
}

// append as a change if the section wasn't previously detected
menuChanges = append(menuChanges, menuChangeSet{Old: "", New: item.Label})
}

// now iterate the previously cached items, noting any that have been deleted
for item := range m.cachedItems {
if _, ok := newCachedMenuData[item]; ok {
continue
}

menuChanges = append(menuChanges, menuChangeSet{Old: item, New: ""})
}

// reset the cached data for the next run
m.cachedItems = newCachedMenuData

return menuChanges, nil
}
102 changes: 102 additions & 0 deletions ee/desktop/runner/menu_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package runner

import (
_ "embed"
"testing"

"github.com/stretchr/testify/require"
)

//go:embed testdata/base_menu.json
var baseMenu []byte

//go:embed testdata/menu_pending_registration.json
var pendingRegistrationMenu []byte

//go:embed testdata/menu_blocked_status.json
var blockedStatusMenu []byte

func Test_menuItemCache_recordMenuUpdates(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args [][]byte
want [][]menuChangeSet
}{
{
name: "startup behavior",
args: [][]byte{baseMenu},
want: [][]menuChangeSet{
[]menuChangeSet{ // we always detect new sections on start up
{"", "tester@example.test"},
{"", "About Kolide..."},
{"", "✔ All Good!"},
{"", "View Details..."},
{"", "Debug"},
},
},
},
{
name: "new pending registration",
args: [][]byte{baseMenu, pendingRegistrationMenu},
want: [][]menuChangeSet{
[]menuChangeSet{
{"", "tester@example.test"},
{"", "About Kolide..."},
{"", "✔ All Good!"},
{"", "View Details..."},
{"", "Debug"},
},
[]menuChangeSet{ // the second run should only view the pending request as new
{"", "❗ Pending Registration Request"},
},
},
},
{
name: "removed pending registration",
args: [][]byte{pendingRegistrationMenu, baseMenu},
want: [][]menuChangeSet{
[]menuChangeSet{
{"", "tester@example.test"},
{"", "About Kolide..."},
{"", "✔ All Good!"},
{"", "View Details..."},
{"", "❗ Pending Registration Request"},
{"", "Debug"},
},
[]menuChangeSet{ // the second run should only remove the pending request
{"❗ Pending Registration Request", ""},
},
},
},
{
name: "moved to blocked status",
args: [][]byte{baseMenu, blockedStatusMenu},
want: [][]menuChangeSet{
[]menuChangeSet{
{"", "tester@example.test"},
{"", "About Kolide..."},
{"", "✔ All Good!"},
{"", "View Details..."},
{"", "Debug"},
},
[]menuChangeSet{ // the second run should detect the status change and the new blocked details section
{"", "Device Blocked"},
{"", "❌ Fix 2 Failing Checks..."},
{"✔ All Good!", ""},
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m := newMenuItemCache()
for idx, menuData := range tt.args {
got, _ := m.recordMenuUpdates(menuData)
require.Equal(t, tt.want[idx], got)
}
})
}
}
24 changes: 19 additions & 5 deletions ee/desktop/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package runner

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
Expand Down Expand Up @@ -127,6 +126,8 @@ type DesktopUsersProcessesRunner struct {
runnerServer *runnerserver.RunnerServer
// osVersion is the version of the OS cached in new
osVersion string
// cachedMenuData is the cached label values of the currently displayed menu data, used for detecting changes
cachedMenuData *menuItemCache
}

// processRecord is used to track spawned desktop processes.
Expand Down Expand Up @@ -163,6 +164,7 @@ func New(k types.Knapsack, messenger runnerserver.Messenger, opts ...desktopUser
usersFilesRoot: agent.TempPath("kolide-desktop"),
processSpawningEnabled: k.DesktopEnabled(),
knapsack: k,
cachedMenuData: newMenuItemCache(),
}

runner.slogger = k.Slogger().With("component", "desktop_runner")
Expand Down Expand Up @@ -357,11 +359,8 @@ func (r *DesktopUsersProcessesRunner) Update(data io.Reader) error {
return errors.New("data is nil")
}

var dataCopy bytes.Buffer
dataTee := io.TeeReader(data, &dataCopy)

// Replace the menu template file
dataBytes, err := io.ReadAll(dataTee)
dataBytes, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("error reading control data: %w", err)
}
Expand Down Expand Up @@ -476,6 +475,21 @@ func (r *DesktopUsersProcessesRunner) generateMenuFile() error {
return err
}

menuChanges, err := r.cachedMenuData.recordMenuUpdates(parsedMenuDataBytes)
if err != nil {
r.slogger.Log(context.TODO(), slog.LevelWarn,
"error recording updates to cached menu items",
"err", err,
)
}

if len(menuChanges) > 0 {
r.slogger.Log(context.TODO(), slog.LevelInfo,
"detected changes to menu bar items",
"changes", menuChanges,
)
}

return nil
}

Expand Down
54 changes: 54 additions & 0 deletions ee/desktop/runner/testdata/base_menu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"icon": "default",
"tooltip": "Kolide - Device Status: All Good",
"items": [
{
"label": "tester@example.test",
"tooltip": null,
"disabled": true,
"items": []
},
{
"action": {
"type": "open-url",
"action": {
"url": "https://example.com/docs/about/overview"
}
},
"label": "About Kolide...",
"tooltip": null,
"disabled": false,
"items": []
},
{
"separator": true
},
{
"label": "✔ All Good!",
"tooltip": null,
"disabled": true,
"items": []
},
{
"action": {
"type": "open-url",
"action": {
"url": "https://app.example.com/1dsfsdfsdfsd"
}
},
"label": "View Details...",
"tooltip": null,
"disabled": false,
"items": []
},
{
"separator": true
},
{
"label": "Debug",
"tooltip": null,
"disabled": false,
"items": []
}
]
}
91 changes: 91 additions & 0 deletions ee/desktop/runner/testdata/menu_blocked_status.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"icon": "circle-x",
"tooltip": "Kolide - Device Status: Blocked",
"items": [
{
"label": "tester@example.test",
"tooltip": null,
"disabled": true,
"items": []
},
{
"action": {
"type": "open-url",
"action": {
"url": "https://example.com/docs/about/overview"
}
},
"label": "About Kolide...",
"tooltip": null,
"disabled": false,
"items": []
},
{
"separator": true
},
{
"label": "Device Blocked",
"tooltip": null,
"disabled": true,
"items": []
},
{
"action": {
"type": "open-url",
"action": {
"url": "https://app.example.com/sdkfgjfdjgidfgd"
}
},
"label": "❌ Fix 2 Failing Checks...",
"tooltip": null,
"disabled": false,
"items": [
{
"action": {
"type": "open-url",
"action": {
"url": "https://app.example.com/sdkfgjfdjgidfgd"
}
},
"label": "❌ something pretty bad",
"tooltip": null,
"disabled": false,
"items": []
},
{
"action": {
"type": "open-url",
"action": {
"url": "https://app.example.com/sdkfgjfdjgidfgd"
}
},
"label": "❌ even worse",
"tooltip": null,
"disabled": false,
"items": []
}
]
},
{
"action": {
"type": "open-url",
"action": {
"url": "https://app.example.com/1dsfsdfsdfsd"
}
},
"label": "View Details...",
"tooltip": null,
"disabled": false,
"items": []
},
{
"separator": true
},
{
"label": "Debug",
"tooltip": null,
"disabled": false,
"items": []
}
]
}
Loading
Loading