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

macos presence detection #1867

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9d05605
first pass poc at macos presence detection
James-Pickett Sep 13, 2024
97f4287
naming
James-Pickett Sep 13, 2024
566d5bd
rough exec implementation of presence detection
James-Pickett Sep 17, 2024
ecd9d3e
add headers to v2CmdRequestType, act on headers in presence detection…
James-Pickett Sep 18, 2024
d7efbab
dont show desktop until requested by runner
James-Pickett Sep 19, 2024
4b99368
do presence detection through desktop server
James-Pickett Sep 20, 2024
c1eb82d
clean up, lint
James-Pickett Sep 20, 2024
635ec1b
look for reason in header with default, remove unneeded struct
James-Pickett Sep 23, 2024
c87a128
put c in own file
James-Pickett Sep 23, 2024
47fbb32
dont use desktop runner singleton
James-Pickett Sep 23, 2024
1fd46c9
build tags in c files
James-Pickett Sep 23, 2024
888643f
presence detection tests
James-Pickett Sep 23, 2024
ddc3ada
test that headers are present after opening krypto challenge
James-Pickett Sep 23, 2024
9524d0d
update log
James-Pickett Sep 24, 2024
cd65542
dont display desktop in test, just manage process
James-Pickett Sep 24, 2024
6f3bb1a
increase runner test start up time
James-Pickett Sep 24, 2024
15a981d
presence detector return durtion since last detection, add to local s…
James-Pickett Sep 26, 2024
aa824b1
tweaks
James-Pickett Sep 26, 2024
8747dd0
skip presence test on non darwin
James-Pickett Sep 26, 2024
e12a332
simplify presence detector
James-Pickett Sep 26, 2024
dcaa01e
presence detection via ec middleware
James-Pickett Sep 27, 2024
98c8c51
feedback
James-Pickett Sep 27, 2024
4d952b2
make presence detection part of test darwin only
James-Pickett Sep 27, 2024
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
7 changes: 5 additions & 2 deletions cmd/launcher/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ func runDesktop(_ *multislogger.MultiSlogger, args []string) error {
}, func(error) {})

shutdownChan := make(chan struct{})
server, err := userserver.New(slogger, *flUserServerAuthToken, *flUserServerSocketPath, shutdownChan, notifier)
showDesktopChan := make(chan struct{})

server, err := userserver.New(slogger, *flUserServerAuthToken, *flUserServerSocketPath, shutdownChan, showDesktopChan, notifier)
if err != nil {
return err
}
Expand Down Expand Up @@ -182,9 +184,10 @@ func runDesktop(_ *multislogger.MultiSlogger, args []string) error {
}
}()

// block until a send on showDesktopChan
<-showDesktopChan
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, this is tidy!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was torn between this route and just passing a flag when we spin up desktop. We are able to show the desktop on demand, but as far as I can tell, there is no way to hide the desktop on demand. When you tell systray to shutdown, it exits the program completely. So if we ever want to hide it, we have to kill the process and let a new one spin up hidden.

I chose this route because I think it's rare that we would turn off the desktop and I don't want to be starting / killing processes on a first install where a user is trying auth for the first time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's rare that we would turn off the desktop

I agree, I think that's a reasonable assumption -- this route makes sense to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think users will, but I think your reasoning is sound.

// blocks until shutdown called
m.Init()

return nil
}

Expand Down
1 change: 1 addition & 0 deletions cmd/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ func runLauncher(ctx context.Context, cancel func(), multiSlogger, systemMultiSl
ls, err := localserver.New(
ctx,
k,
runner,
)

if err != nil {
Expand Down
108 changes: 79 additions & 29 deletions ee/desktop/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/kolide/launcher/ee/desktop/user/client"
"github.com/kolide/launcher/ee/desktop/user/menu"
"github.com/kolide/launcher/ee/desktop/user/notify"
"github.com/kolide/launcher/ee/presencedetection"
"github.com/kolide/launcher/ee/ui/assets"
"github.com/kolide/launcher/pkg/backoff"
"github.com/kolide/launcher/pkg/rungroup"
Expand Down Expand Up @@ -118,9 +119,6 @@ type DesktopUsersProcessesRunner struct {
// usersFilesRoot is the launcher root dir with will be the parent dir
// for kolide desktop files on a per user basis
usersFilesRoot string
// processSpawningEnabled controls whether or not desktop user processes are automatically spawned
// This effectively represents whether or not the launcher desktop GUI is enabled or not
processSpawningEnabled bool
// knapsack is the almighty sack of knaps
knapsack types.Knapsack
// runnerServer is a local server that desktop processes call to monitor parent
Expand Down Expand Up @@ -155,17 +153,16 @@ func (pr processRecord) String() string {
// New creates and returns a new DesktopUsersProcessesRunner runner and initializes all required fields
func New(k types.Knapsack, messenger runnerserver.Messenger, opts ...desktopUsersProcessesRunnerOption) (*DesktopUsersProcessesRunner, error) {
runner := &DesktopUsersProcessesRunner{
interrupt: make(chan struct{}),
uidProcs: make(map[string]processRecord),
updateInterval: k.DesktopUpdateInterval(),
menuRefreshInterval: k.DesktopMenuRefreshInterval(),
procsWg: &sync.WaitGroup{},
interruptTimeout: time.Second * 5,
hostname: k.KolideServerURL(),
usersFilesRoot: agent.TempPath("kolide-desktop"),
processSpawningEnabled: k.DesktopEnabled(),
knapsack: k,
cachedMenuData: newMenuItemCache(),
interrupt: make(chan struct{}),
uidProcs: make(map[string]processRecord),
updateInterval: k.DesktopUpdateInterval(),
menuRefreshInterval: k.DesktopMenuRefreshInterval(),
procsWg: &sync.WaitGroup{},
interruptTimeout: time.Second * 5,
hostname: k.KolideServerURL(),
usersFilesRoot: agent.TempPath("kolide-desktop"),
knapsack: k,
cachedMenuData: newMenuItemCache(),
}

runner.slogger = k.Slogger().With("component", "desktop_runner")
Expand Down Expand Up @@ -286,6 +283,29 @@ func (r *DesktopUsersProcessesRunner) Interrupt(_ error) {
)
}

func (r *DesktopUsersProcessesRunner) DetectPresence(reason string, interval time.Duration) (time.Duration, error) {
if r.uidProcs == nil || len(r.uidProcs) == 0 {
return presencedetection.DetectionFailedDurationValue, errors.New("no desktop processes running")
}

var lastErr error
var lastDurationSinceLastDetection time.Duration

for _, proc := range r.uidProcs {
client := client.New(r.userServerAuthToken, proc.socketPath)
lastDurationSinceLastDetection, err := client.DetectPresence(reason, interval)

if err != nil {
lastErr = err
continue
}

return lastDurationSinceLastDetection, nil
}

return lastDurationSinceLastDetection, fmt.Errorf("no desktop processes detected presence, last error: %w", lastErr)
}

// killDesktopProcesses kills any existing desktop processes
func (r *DesktopUsersProcessesRunner) killDesktopProcesses(ctx context.Context) {
wgDone := make(chan struct{})
Expand Down Expand Up @@ -452,12 +472,35 @@ func (r *DesktopUsersProcessesRunner) Update(data io.Reader) error {
}

func (r *DesktopUsersProcessesRunner) FlagsChanged(flagKeys ...keys.FlagKey) {
if slices.Contains(flagKeys, keys.DesktopEnabled) {
r.processSpawningEnabled = r.knapsack.DesktopEnabled()
r.slogger.Log(context.TODO(), slog.LevelDebug,
"runner processSpawningEnabled set by control server",
"process_spawning_enabled", r.processSpawningEnabled,
)
if !slices.Contains(flagKeys, keys.DesktopEnabled) {
return
}

r.slogger.Log(context.TODO(), slog.LevelDebug,
"desktop enabled set by control server",
"desktop_enabled", r.knapsack.DesktopEnabled(),
)

if !r.knapsack.DesktopEnabled() {
// there is no way to "hide" the menu, so we will just kill any existing processes
// they will respawn in "silent" mode
r.killDesktopProcesses(context.TODO())
return
}

// DesktopEnabled() == true
// Tell any running desktop user processes that they should show the menu
for uid, proc := range r.uidProcs {
client := client.New(r.userServerAuthToken, proc.socketPath)
if err := client.ShowDesktop(); err != nil {
r.slogger.Log(context.TODO(), slog.LevelError,
"sending refresh command to user desktop process",
"uid", uid,
"pid", proc.Process.Pid,
"path", proc.path,
"err", err,
)
}
}
}

Expand All @@ -483,6 +526,10 @@ func (r *DesktopUsersProcessesRunner) writeSharedFile(path string, data []byte)

// refreshMenu updates the menu file and tells desktop processes to refresh their menus
func (r *DesktopUsersProcessesRunner) refreshMenu() {
if !r.knapsack.DesktopEnabled() {
return
}

if err := r.generateMenuFile(); err != nil {
if r.knapsack.DebugServerData() {
r.slogger.Log(context.TODO(), slog.LevelError,
Expand All @@ -503,7 +550,6 @@ func (r *DesktopUsersProcessesRunner) refreshMenu() {
for uid, proc := range r.uidProcs {
client := client.New(r.userServerAuthToken, proc.socketPath)
if err := client.Refresh(); err != nil {

r.slogger.Log(context.TODO(), slog.LevelError,
"sending refresh command to user desktop process",
"uid", uid,
Expand Down Expand Up @@ -601,12 +647,6 @@ func (r *DesktopUsersProcessesRunner) runConsoleUserDesktop() error {
return nil
}

if !r.processSpawningEnabled {
// Desktop is disabled, kill any existing desktop user processes
r.killDesktopProcesses(context.Background())
return nil
}

executablePath, err := r.determineExecutablePath()
if err != nil {
return fmt.Errorf("determining executable path: %w", err)
Expand Down Expand Up @@ -669,13 +709,23 @@ func (r *DesktopUsersProcessesRunner) spawnForUser(ctx context.Context, uid stri
r.waitOnProcessAsync(uid, cmd.Process)

client := client.New(r.userServerAuthToken, socketPath)
if err := backoff.WaitFor(client.Ping, 10*time.Second, 1*time.Second); err != nil {

pingFunc := client.Ping
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved

// if the desktop is enabled, we want to show the desktop
// just perform this instead of ping to verify the desktop is running
// and show it right away
if r.knapsack.DesktopEnabled() {
pingFunc = client.ShowDesktop
}

if err := backoff.WaitFor(pingFunc, 10*time.Second, 1*time.Second); err != nil {
// unregister proc from desktop server so server will not respond to its requests
r.runnerServer.DeRegisterClient(uid)

if err := cmd.Process.Kill(); err != nil {
r.slogger.Log(ctx, slog.LevelError,
"killing user desktop process after startup ping failed",
"killing user desktop process after startup ping / show desktop failed",
"uid", uid,
"pid", cmd.Process.Pid,
"path", cmd.Path,
Expand Down
2 changes: 1 addition & 1 deletion ee/desktop/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestDesktopUserProcessRunner_Execute(t *testing.T) {
}()

// let it run a few intervals
time.Sleep(r.updateInterval * 3)
time.Sleep(r.updateInterval * 6)
r.Interrupt(nil)

user, err := user.Current()
Expand Down
39 changes: 39 additions & 0 deletions ee/desktop/user/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"time"

"github.com/kolide/launcher/ee/desktop/user/notify"
"github.com/kolide/launcher/ee/desktop/user/server"
"github.com/kolide/launcher/ee/presencedetection"
)

type transport struct {
Expand Down Expand Up @@ -55,6 +59,41 @@ func (c *client) Refresh() error {
return c.get("refresh")
}

func (c *client) ShowDesktop() error {
return c.get("show")
}

func (c *client) DetectPresence(reason string, interval time.Duration) (time.Duration, error) {
encodedReason := url.QueryEscape(reason)
encodedInterval := url.QueryEscape(interval.String())

resp, requestErr := c.base.Get(fmt.Sprintf("http://unix/detect_presence?reason=%s&interval=%s", encodedReason, encodedInterval))
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
if requestErr != nil {
return presencedetection.DetectionFailedDurationValue, fmt.Errorf("getting presence: %w", requestErr)
}

var response server.DetectPresenceResponse
if resp.Body != nil {
defer resp.Body.Close()

if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return presencedetection.DetectionFailedDurationValue, fmt.Errorf("decoding response: %w", err)
}
}

var detectionErr error
if response.Error != "" {
detectionErr = errors.New(response.Error)
}

durationSinceLastDetection, parseErr := time.ParseDuration(response.DurationSinceLastDetection)
if parseErr != nil {
return presencedetection.DetectionFailedDurationValue, fmt.Errorf("parsing time since last detection: %w", parseErr)
}

return durationSinceLastDetection, detectionErr
}

func (c *client) Notify(n notify.Notification) error {
notificationToSend := notify.Notification{
Title: n.Title,
Expand Down
2 changes: 1 addition & 1 deletion ee/desktop/user/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestClient_GetAndShutdown(t *testing.T) {

socketPath := testSocketPath(t)
shutdownChan := make(chan struct{})
server, err := server.New(multislogger.NewNopLogger(), validAuthToken, socketPath, shutdownChan, nil)
server, err := server.New(multislogger.NewNopLogger(), validAuthToken, socketPath, shutdownChan, make(chan<- struct{}), nil)
require.NoError(t, err)

go func() {
Expand Down
Loading
Loading