#!/usr/bin/python ################################################################################ # Copyright (c) 2017 University of Utah Student Computing Labs. # All Rights Reserved. # # Author: Sam Forester sam.forester@utah.edu # # Permission to use, copy, modify, and distribute this software and # its documentation for any purpose and without fee is hereby granted, # provided that the above copyright notice appears in all copies and # that both that copyright notice and this permission notice appear # in supporting documentation, and that the name of The University # of Utah not be used in advertising or publicity pertaining to # distribution of the software without specific, written prior # permission. This software is supplied as is without expressed or # implied warranties of any kind. ################################################################################ import sys import subprocess import plistlib import SystemConfiguration as sc import threading import time import re import os ''' Author's Notes: Here's the basic idea of my script described in the blog: https://apple.lib.utah.edu/jamfhelper The script on it's own doesn't do much as I stripped out the functional maintenance portions, but it should illustrate how I used the jamfHelper command. It is designed to work on Mac laptops, but functionally works on any mac with JAMF installed. On desktops, power connectivity isn't much of an issue for obvious reasons. I've wanted to make this script better for a while. Currently it spawns a jamfHelper window with the status of both power and ethernet connections, but if the jamfHelper window is just left there without interaction while the ethernet and power adapters states are changed, the jamfHelper window is static and doesn't update. The script goes through and is pretty thorough about checking the status of power and ethernet after each interaction, but I would still like main window to dynamically update on the change of status. The Success and Failure classes were fun to make, python has some pretty cool features they probably aren't necessary, but I was all about having a variable to test as well as print. I have two versions of check_power() both work the exact same but one uses another framework. This was my first attempt with threading so bear that in mind, it's no fun to code if you can't try something new and/or learn something. Use the snippets or extend it to your needs. You can comment below if you have any questions. ''' class Success(object): ''' simple class that tests true and prints as unicode checkmark ''' def __str__(self): # return u"{}".format(u'\u2705') # checkmark return u"{}".format(u'\u2705') # White Heavy Check Mark def __bool__(self): return True __nonzero__ = __bool__ class Failure(object): ''' simple class that tests False and prints as unicode X ''' def __str__(self): return u"{}".format(u'\u274C') # Cross Mark (i.e. red X)) def __bool__(self): return False __nonzero__ = __bool__ def check_ethernet(): ''' checks active network interface hardware, if Ethernet, returns success() ''' dynamicStoreRef = sc.SCDynamicStoreCreate(None, sys.argv[0], None, None) active_key = 'State:/Network/Global/IPv4' activeService = sc.SCDynamicStoreCopyValue(dynamicStoreRef, active_key) if activeService: serviceUUID = activeService['PrimaryService'] key = 'Setup:/Network/Service/{}/Interface'.format(serviceUUID) service = sc.SCDynamicStoreCopyValue(dynamicStoreRef, key) hardware = service['Hardware'] else: # if no network services are active, then obviously ethernet isn't connected return Failure() # return the class with which we can both test and print its unicode character if 'Ethernet' in hardware: return Success() else: return Failure() def check_power(): ''' Simple command to check for a power adapter on a Mac laptop. If connected to power, returns success() ''' out = subprocess.check_output(['pmset', '-g', 'ps']) if 'AC' in out: return Success() else: return Failure() def check_power2(): ''' Does the exact same thing as check_power(), but using the SystemConfiguration framework instead. ''' dynamicStoreRef = sc.SCDynamicStoreCreate(None, sys.argv[0], None, None) power_key = 'State:/IOKit/PowerAdapter' power_active = sc.SCDynamicStoreCopyValue(dynamicStoreRef, power_key) # return the class with which we can both test and print its unicode character if power_active: return Success() else: return Failure() def main_display_window(): ''' The main jamfHelper window display. We are just dynamically building the jamfHelper window as events happen. Automatically changing the description as tests succeed/fail and changing the text ''' # get the initial status of the ethernet and power connections ethernet = check_ethernet() power = check_power() # because of the Success() and Failure() classes we can use them to print out a # unicode character as well as test them for True|False desc = "Before running maintenance make sure the computer is:\n\n" + \ u"{} Connected to Power\n".format(power) + \ u"{} Connected to Ethernet\n".format(ethernet) # If both services are up then we can just run maintenance (otherwise we'll have to wait) if ethernet and power: button = 'Run' else: button = 'Connect' # default icon located in the jamfHelper.app (we use other images) icon = '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/Resources/Message.png' # jamfHelper command helper_cmd = [ '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper', '-windowType', 'utility', '-windowPosition', 'lr', '-iconSize', '145', '-icon', icon, # requires a valid path to image file '-title', 'Error', '-description', desc, # change the heading by a few characters and see the wildly different # window sizing issues I was talking about '-heading', 'An error was encountered...', '-button1', button, '-defaultButton', '1', ] pipe = subprocess.PIPE p = subprocess.Popen(helper_cmd, stdout=pipe, stderr=pipe) # this isn't really necessary in this script, but if you had more than one button # you would want to test this variable to see what button was pressed retcode, err = p.communicate() # retcode returned as string return int(retcode) # easier to test an int def detect_ethernet_window(): ''' Similar to detect_power_window(), we are threading out a jamfHelper window and checking the status of an ethernet network conenction. Once the network connection is connected, it automatically closes the jamfHelper window ''' # default icon located in the jamfHelper.app (we use other images) icon = '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/Resources/Message.png' # jamfHelper command helper_cmd = [ '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper', '-windowType', 'utility', '-windowPosition', 'lr', '-title', 'Connecting...', '-heading', 'Waiting for Ethernet...', '-description', 'Please plug in Thunderbolt Ethernet adapter (this may take a moment)', '-icon', icon, '-iconSize', '170', '-button1', 'Cancel', '-defaultButton', '1', ] # get current status of ethernet network connection ethernet = check_ethernet() # Here we thread out a jamfHelper window and check the status of ethernet every second # but only spawn a thread if we need to (i.e. ethernet isn't already connected) if not ethernet: p = subprocess.Popen(helper_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # create a new thread for the helper window t = threading.Thread(target=p.communicate) t.start() # while the status of check_ethernet() fails or the thread lives while not ethernet and t.isAlive(): ethernet = check_ethernet() time.sleep(1) # kill the jamfHelper thread if it's still running if t.isAlive(): subprocess.check_call(['kill', str(p.pid)]) # bring the threads back together t.join() return ethernet def detect_power_window(): ''' Similar to detect_ethernet_window(), we are threading out a jamfHelper window and checking the status of the power adapter. Once the power adapter is connected, it automatically closes the jamfHelper window ''' # default icon located in the jamfHelper.app (we use other images) icon = '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/Resources/Message.png' # jamfHelper command helper_cmd = [ '/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper', '-windowType', 'utility', '-windowPosition', 'lr', '-title', 'Connecting...', '-heading', 'Waiting for Power...', '-description', 'Please plug in AC Power adapter', '-icon', icon, '-iconSize', '150', '-button1', 'Cancel', '-defaultButton', '1', ] # get current status of power adapter power = check_power() # Here we thread out a jamfHelper window and check the status of power every second # only spawn a thread if we need to (i.e. power isn't already connected) if not power: p = subprocess.Popen(helper_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # create a new thread for the helper window t = threading.Thread(target=p.communicate) t.start() # while the status of check_power() fails or the thread lives while not power and t.isAlive(): power = check_power() time.sleep(1) # kill the jamfHelper thread if it's still running if t.isAlive(): subprocess.check_call(['kill', str(p.pid)]) # bring the threads back together t.join() return power def run_maintenance(): ''' This is where we kick off our mainentenance program at the Marriott Library, but because this is so institutionally specific, I modified the actual code for the blog post. ''' # Do something interesting here! print('running maintenance!') sys.exit() def main(): while True: main_display_window() if check_ethernet() and check_power(): run_maintenance() else: detect_ethernet_window() detect_power_window() if __name__ == '__main__': main()