|
#!/usr/bin/python |
|
# -*- coding: utf-8 -*- |
|
# Copyright (C) 2016 Shea G Craig |
|
# |
|
# This program is free software: you can redistribute it and/or modify |
|
# it under the terms of the GNU General Public License as published by |
|
# the Free Software Foundation, either version 3 of the License, or |
|
# (at your option) any later version. |
|
# |
|
# This program is distributed in the hope that it will be useful, |
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
# GNU General Public License for more details. |
|
# |
|
# You should have received a copy of the GNU General Public License |
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
|
|
""" |
|
Help user set default mail client despite OS X bug. |
|
|
|
Sets the default mail reader to Apple Mail (because we configure our |
|
clients to use Outlook) using the LaunchServices framework, and then |
|
immediately log out. |
|
|
|
Displays a dialog instructing user to close open applications that may |
|
block logout from occuring, rather than execute a Volcanic Instant Death |
|
Logout. |
|
|
|
This uses code from my auto_logout project for the applescripting and |
|
alert dialog presentation. |
|
|
|
This was written to be part of an OnDemand item in Munki's Managed |
|
Software Center, using outset's on-demand feature to run as the current |
|
console user (as it would otherwise run as root and not work). |
|
""" |
|
|
|
|
|
import os |
|
import subprocess |
|
import sys |
|
|
|
# pylint: disable=no-name-in-module |
|
from AppKit import (NSImage, NSAlert, NSTimer, NSRunLoop, NSApplication, |
|
NSSound, NSModalPanelRunLoopMode, NSApp, |
|
NSRunAbortedResponse, NSAlertFirstButtonReturn) |
|
# pylint: enable=no-name-in-module |
|
from LaunchServices import LSSetDefaultHandlerForURLScheme |
|
from SystemConfiguration import SCDynamicStoreCopyConsoleUser |
|
|
|
|
|
# Sound played when alert is presented. See README. |
|
ALERT_SOUND = "Submarine" |
|
# Icon used in the alerts. If not present, the Python rocket is used |
|
# instead. |
|
ICON = "/usr/local/sas/sas.png" |
|
|
|
|
|
# Methods are named according to PyObjC/Cocoa style. |
|
# pylint: disable=invalid-name |
|
class Alert(NSAlert): |
|
"""Subclasses NSAlert to include a timeout.""" |
|
|
|
def init(self): # pylint: disable=super-on-old-class |
|
"""Add an instance variable for our timer.""" |
|
self = super(Alert, self).init() |
|
self.timer = None |
|
self.alert_sound = None |
|
return self |
|
|
|
def setIconWithContentsOfFile_(self, path): |
|
"""Convenience method for adding an icon. |
|
Args: |
|
path: String path to a valid NSImage filetype (png) |
|
""" |
|
icon = NSImage.alloc().initWithContentsOfFile_(path) |
|
self.setIcon_(icon) # pylint: disable=no-member |
|
|
|
def setAlertSound_(self, name): |
|
"""Set the sound to play when alert is presented. |
|
Args: |
|
name: String name of a system sound. See the README. |
|
""" |
|
self.alert_sound = name |
|
|
|
def setTimeToGiveUp_(self, time): |
|
"""Configure alert to give up after time seconds.""" |
|
# Cocoa objects must use class func alloc().init(), so pylint |
|
# doesn't see our init(). |
|
# pylint: disable=attribute-defined-outside-init |
|
self.timer = \ |
|
NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_( |
|
time, self, "_killWindow", None, False) |
|
# pylint: enable=attribute-defined-outside-init |
|
|
|
def present(self): |
|
"""Present the Alert, giving up after configured time.. |
|
Returns: Int result code, based on PyObjC enums. See NSAlert |
|
Class reference, but result should be one of: |
|
User clicked the cancel button: |
|
NSAlertFirstButtonReturn = 1000 |
|
Alert timed out: |
|
NSRunAbortedResponse = -1001 |
|
""" |
|
if self.timer: |
|
NSRunLoop.currentRunLoop().addTimer_forMode_( |
|
self.timer, NSModalPanelRunLoopMode) |
|
# Start a Cocoa application by getting the shared app object. |
|
# Make the python app the active app so alert is noticed. |
|
app = NSApplication.sharedApplication() |
|
app.activateIgnoringOtherApps_(True) |
|
if self.alert_sound: |
|
sound = NSSound.soundNamed_(self.alert_sound).play() |
|
result = self.runModal() # pylint: disable=no-member |
|
print result |
|
return result |
|
|
|
# pylint: disable=no-self-use |
|
def _killWindow(self): |
|
"""Abort the modal window as managed by NSApp.""" |
|
NSApp.abortModal() |
|
# pylint: enable=no-self-use |
|
|
|
# pylint: enable=no-init |
|
# pylint: enable=invalid-name |
|
|
|
|
|
def build_alert(): |
|
"""Build an alert for auto-logout notifications.""" |
|
alert = Alert.alloc().init() # pylint: disable=no-member |
|
alert.setMessageText_( |
|
"Setting the default mail reader requires an immediate logout " |
|
"due to a bug in OS X.") |
|
alert.setInformativeText_("Please quit all applications and hit 'Okay'. " |
|
"Your computer will then logout.") |
|
alert.addButtonWithTitle_("Okay") |
|
alert.addButtonWithTitle_("Cancel") |
|
alert.setIconWithContentsOfFile_(ICON) |
|
alert.setAlertSound_(ALERT_SOUND) |
|
return alert |
|
|
|
|
|
def set_mail_reader(bundle_id): |
|
"""Use LaunchServices to set mailto handler. |
|
|
|
There is a bug in OS X that allows you to set this only once. |
|
Afterwards, if you set it again, it will revert to the previous |
|
setting within about 10 seconds. Until this is fixed, logging out |
|
really quickly seems to work around it. |
|
|
|
Args: |
|
bundle_id (String): Bundle Identifier for the app to handle |
|
mail. Caps do not seem to matter. |
|
|
|
Returns: |
|
Integer return code (0 is a success) as per |
|
https://developer.apple.com/library/mac/documentation/Carbon/Reference/LaunchServicesReference/ |
|
""" |
|
return LSSetDefaultHandlerForURLScheme("mailto", bundle_id) |
|
|
|
|
|
def really_log_out(): |
|
"""Log out without the prompt. Will still ask about open apps.""" |
|
run_applescript('tell application "loginwindow" to «event aevtrlgo»') |
|
|
|
|
|
def run_applescript(script): |
|
"""Run an applescript""" |
|
process = subprocess.Popen(['osascript', '-'], stdout=subprocess.PIPE, |
|
stdin=subprocess.PIPE, stderr=subprocess.PIPE) |
|
result, err = process.communicate(script) |
|
|
|
if err: |
|
raise Exception(err) |
|
return process.returncode |
|
|
|
|
|
def build_abort_alert(): |
|
"""Build an alert for letting user know it failed.""" |
|
alert = Alert.alloc().init() # pylint: disable=no-member |
|
alert.setMessageText_( |
|
"Failed to set default mail handler") |
|
alert.setInformativeText_("Please contact the Helpdesk.") |
|
alert.addButtonWithTitle_("Okay") |
|
alert.addButtonWithTitle_("Cancel") |
|
alert.setIconWithContentsOfFile_(ICON) |
|
alert.setAlertSound_(ALERT_SOUND) |
|
return alert |
|
|
|
|
|
def main(): |
|
alert = build_alert() |
|
if alert.present() != NSAlertFirstButtonReturn: |
|
print "User Cancelled" |
|
sys.exit() |
|
else: |
|
if set_mail_reader("com.apple.mail") == 0: |
|
really_log_out() |
|
else: |
|
abort_alert = build_abort_alert() |
|
alert.present() |
|
sys.exit(1) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |