From 02958c0b4ef02fca9c364e49a80fb77e528fb333 Mon Sep 17 00:00:00 2001 From: Erik Gomez Date: Fri, 31 Jan 2020 14:55:10 -0600 Subject: [PATCH] Add new aggressive UX for dismissal_count_threshold --- README.md | 9 ++- payload/Library/nudge/Resources/nudge | 92 ++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4d4af75..4606789 100755 --- a/README.md +++ b/README.md @@ -123,6 +123,13 @@ This is the second set of text above the **Update Machine** button. "button_sub_titletext": "Click on the button below." ``` +### Dismissal Count Threshold +This is the amount of times a user can disregard nudge before more aggressive behaviors kick in. + +```json +"dismissal_count_threshold": 100 +``` + ### URL for self-servicing upgrade app This is the full URL for a local self-servicing app such as Jamf Self Service or Munki Managed Software Center linking directly to a Jamf @@ -211,7 +218,7 @@ This is the path to the macOS installer application. Note: This setting is ignored when `local_url_for_upgrade` is provided. ### Days Between Notifications -Instead of having the Nudge GUI appear every half hour, make sure there is at least this many days between notifications. +Instead of having the Nudge GUI appear every half hour, make sure there is at least this many days between notifications. *Note*: if you set this to something other than 0, it may not be evaluated in full 24-hour increments. For example, if the Nudge GUI appeared on Monday in the afternoon, it may appear Tuesday morning. ```json "days_between_notifications": 0 diff --git a/payload/Library/nudge/Resources/nudge b/payload/Library/nudge/Resources/nudge index e5ad773..1b7bde5 100755 --- a/payload/Library/nudge/Resources/nudge +++ b/payload/Library/nudge/Resources/nudge @@ -16,9 +16,10 @@ import urllib.request, urllib.parse, urllib.error import webbrowser from datetime import datetime, timedelta from distutils.version import LooseVersion +from urllib.parse import urlparse, unquote import Foundation import objc -from AppKit import NSApplication, NSImage +from AppKit import * from CoreFoundation import CFPreferencesCopyAppValue, CFPreferencesSetAppValue, CFPreferencesAppSynchronize from SystemConfiguration import SCDynamicStoreCopyConsoleUser @@ -29,34 +30,92 @@ import gurl class timerController(Foundation.NSObject): '''Thanks to frogor for help in figuring this part out''' def activateWindow_(self, timer_obj): - nudgelog('Re-activating .nib to the foreground') - # Move the application to the front - NSApplication.sharedApplication().activateIgnoringOtherApps_(True) - # Move the main window to the front - # Nibbler objects have a .win property (...should probably be .window) - # that contains a reference to the first NSWindow it finds - nudge.win.makeKeyAndOrderFront_(None) + determine_state_and_nudge() + + +def determine_state_and_nudge(): + '''Determine the state of nudge and re-fresh window''' + workspace = NSWorkspace.sharedWorkspace() + currently_active = NSApplication.sharedApplication().isActive() + frontmost_app = workspace.frontmostApplication().bundleIdentifier() + # Setup these globals as we will potentially override them + global NUDGE_DISMISSED_COUNT + global ACCEPTABLE_APPS + if not currently_active and frontmost_app not in ACCEPTABLE_APPS: + nudgelog('Nudge or acceptable applications not currently active') + # If this is the under max dismissed count, just bring nudge back to the forefront + # This is the old behavior + if NUDGE_DISMISSED_COUNT < DISMISSAL_COUNT_THRESHOLD: + nudgelog('Nudge dismissed count under threshold') + NUDGE_DISMISSED_COUNT += 1 + bring_nudge_to_forefront() + else: + # Get more aggressive - new behavior + nudgelog('Nudge dismissed count over threshold') + NUDGE_DISMISSED_COUNT += 1 + nudgelog('Enforcing acceptable applications') + # Loop through all the running applications + for app in NSWorkspace.sharedWorkspace().runningApplications(): + app_name = str(app.bundleIdentifier()) + app_bundle = str(app.bundleURL()) + if app_bundle: + # The app bundle contains file://, quoted path and trailing slashes + app_bundle_path = unquote(urlparse(app_bundle).path).rstrip('\/') + # Add Software Update pane or macOS upgrade app to acceptable app list + if app_bundle_path == PATH_TO_APP: + ACCEPTABLE_APPS.append(app_name) + else: + # Some of the apps from NSWorkspace don't have bundles, so force empty string + app_bundle_path = '' + # Hide any apps that are not in acceptable list or are not the macOS upgrade app + if (app_name not in ACCEPTABLE_APPS) or (app_bundle_path != PATH_TO_APP): + app.hide() + # Race condition with NSWorkspace. Python is faster :) + time.sleep(0.001) + # Another small sleep to ensure we can bring Nudge on top + time.sleep(0.5) + bring_nudge_to_forefront() + # Pretend to open the button and open the update mechanism + button_update(True) + + +def bring_nudge_to_forefront(): + '''Brings nudge to the forefront - old behavior''' + nudgelog('Nudge not active - Activating to the foreground') + # We have to bring back python to the forefront since nibbler is a giant cheat + NSApplication.sharedApplication().activateIgnoringOtherApps_(True) + # Now bring the nudge window itself to the forefront + # Nibbler objects have a .win property (...should probably be .window) + # that contains a reference to the first NSWindow it finds + nudge.win.makeKeyAndOrderFront_(None) def button_moreinfo(): '''Open browser more info button''' + nudgelog('User clicked on more info button - opening URL in default browser') webbrowser.open_new_tab(MORE_INFO_URL) -def button_update(): +def button_update(simulated_click=False): '''Start the update process''' + if simulated_click: + nudgelog('Simulated click on update button - opening update application') + else: + nudgelog('User clicked on update button - opening update application') cmd = ['/usr/bin/open', PATH_TO_APP] subprocess.Popen(cmd) def button_ok(): '''Quit out of nudge if user hits the ok button''' + nudgelog('User clicked on ok button - exiting application') nudge.quit() def button_understand(): '''Add an extra button to force the user to read the dialog, prior to being able to exit the UI.''' + nudgelog('User clicked on understand button - enabling ok button') nudge.views['button.understand'].setHidden_(True) nudge.views['button.ok'].setHidden_(False) nudge.views['button.ok'].setEnabled_(True) @@ -128,6 +187,7 @@ def get_os_version(): '''Return OS version.''' return LooseVersion(platform.mac_ver()[0]) + def get_os_version_major(): '''Return major OS version.''' full_os = platform.mac_ver()[0] @@ -141,6 +201,7 @@ def get_os_version_major(): nudgelog('Cannot reliably determine OS major version. Exiting...') exit(1) + def get_parsed_options(): '''Return the parsed options and args for this application.''' # Options @@ -312,13 +373,24 @@ def main(): exit(0) # Setup our globals to use across nibbler and nibbler functions + global DISMISSAL_COUNT_THRESHOLD global NUDGE_PATH global MORE_INFO_URL global PATH_TO_APP + global NUDGE_DISMISSED_COUNT + global ACCEPTABLE_APPS # Figure out the local path of nudge NUDGE_PATH = os.path.dirname(os.path.realpath(__file__)) + # Part for enhanced enforcement of Nudge + NUDGE_DISMISSED_COUNT = 0 + ACCEPTABLE_APPS = [ + 'com.apple.loginwindow', + 'com.apple.systempreferences', + 'org.python.python' + ] + # local json path - if it exists already, let's assume someone is bundling # it with their package. Otherwise check for it and use gurl. json_path = os.path.join(NUDGE_PATH, 'nudge.json') @@ -383,6 +455,7 @@ def main(): cut_off_date_warning = nudge_prefs.get('cut_off_date_warning', 3) days_between_notifications = nudge_prefs.get('days_between_notifications', 0) + DISMISSAL_COUNT_THRESHOLD = nudge_prefs.get('dismissal_count_threshold', 9999999) logo_path = nudge_prefs.get('logo_path', 'company_logo.png') main_subtitle_text = nudge_prefs.get('main_subtitle_text', 'A friendly reminder from your local IT team') @@ -421,6 +494,7 @@ def main(): update_minor = False else: nudgelog('Target OS subversion: %s' % minimum_os_sub_build_version) + nudgelog('Dismissal count threshold: %s ' % DISMISSAL_COUNT_THRESHOLD) # cleanup the tmp stuff now