#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2011-2017 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
logouthelper

Created by Greg Neagle on 2011-06-21.

A helper tool for forced logouts to allow munki to force install items by
a certain deadline.

"""

# standard libs
import os
import time

# our libs
from munkilib import info
from munkilib import munkilog
from munkilib import osutils
from munkilib import prefs
from munkilib import processes
from munkilib import FoundationPlist

# Apple frameworks via PyObjC
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
# No name 'Foo' in module 'Bar' warnings. Disable them.
# pylint: disable=E0611
from Foundation import NSDate
from Foundation import NSDictionary
from Foundation import NSDistributedNotificationCenter
from Foundation import NSNotificationDeliverImmediately
from Foundation import NSNotificationPostToAllSessions
# pylint: enable=E0611


NOTIFICATION_MINS = [240, 180, 120, 90, 60, 45, 30, 15, 10, 5]
MANDATORY_NOTIFICATIONS = [60, 30, 10, 5]
PROCESS_ID = 'com.googlecode.munki.logouthelper'


def log(msg):
    '''Logs messages from this tool with an identifier'''
    munkilog.log('%s: %s' % (PROCESS_ID, msg))


def earliest_force_install_date():
    '''Check installable packages for force_install_after_dates
    Returns None or earliest force_install_after_date converted to local time
    '''
    earliest_date = None

    managed_install_dir = prefs.pref('ManagedInstallDir')
    installinfo_types = {
        'InstallInfo.plist' : 'managed_installs',
        'AppleUpdates.plist': 'AppleUpdates'
    }
    installinfopath = os.path.join(managed_install_dir, 'InstallInfo.plist')

    try:
        installinfo = FoundationPlist.readPlist(installinfopath)
    except FoundationPlist.NSPropertyListSerializationException:
        return None

    for plist_name in installinfo_types:
        key_to_check = installinfo_types[plist_name]
        plist_path = os.path.join(managed_install_dir, plist_name)
        try:
            installinfo = FoundationPlist.readPlist(plist_path)
        except FoundationPlist.NSPropertyListSerializationException:
            continue

        for install in installinfo.get(key_to_check, []):
            force_install_date = install.get('force_install_after_date')

            if force_install_date:
                force_install_date = info.subtract_tzoffset_from_date(
                    force_install_date)
                if not earliest_date or force_install_date < earliest_date:
                    earliest_date = force_install_date

    return earliest_date


def alert_user_of_forced_logout(info_dict=None):
    '''Uses Managed Software Center.app to notify the user of an
    upcoming forced logout.

    Args:
      info: dict of data to send with the notification.
    '''
    consoleuser = osutils.getconsoleuser()
    if not processes.find_processes(
            exe="/Applications/Managed Software Center.app",
            user=consoleuser):
        # Managed Software Center.app isn't running.
        # Use our LaunchAgent to start
        # Managed Software Center.app in the user context.
        launchfile = '/var/run/com.googlecode.munki.ManagedSoftwareCenter'
        fileref = open(launchfile, 'w')
        fileref.close()
        # now wait a bit for it to launch before proceeding
        # because if we don't, sending the logoutwarn notification
        # may fall on deaf ears.
        time.sleep(5)
        if os.path.exists(launchfile):
            os.unlink(launchfile)

    # if set, convert Python dictionary to NSDictionary.
    if info_dict is not None:
        info_dict = NSDictionary.dictionaryWithDictionary_(info_dict)
    # cause MSC.app to display the Forced Logout warning
    dnc = NSDistributedNotificationCenter.defaultCenter()
    dnc.postNotificationName_object_userInfo_options_(
        'com.googlecode.munki.ManagedSoftwareUpdate.logoutwarn',
        None, info_dict,
        NSNotificationDeliverImmediately + NSNotificationPostToAllSessions)

    # make sure flag is in place to cause munki to install at logout
    fileref = open('/private/tmp/com.googlecode.munki.installatlogout', 'w')
    fileref.close()


def main():
    '''Check for logged-in users and upcoming forced installs;
    notify the user if needed; sleep a minute and do it again.'''
    if prefs.pref('LogToSyslog'):
        munkilog.configure_syslog()

    log('launched')
    sent_notifications = []
    logout_time_override = None

    # datetime of now plus largest MANDATORY_NOTIFICATIONS value (with padding).
    minimum_notifications_logout_time = NSDate.date().addTimeInterval_(
        60 * max(MANDATORY_NOTIFICATIONS) + 30)
    while True:
        if not osutils.currentGUIusers():
            # no-one is logged in, so bail
            log('no-one logged in')
            time.sleep(10)  # makes launchd happy
            log('exited')
            exit(0)

        # we check each time because items might have been added or removed
        # from the list; or their install date may have been changed.
        next_logout_time = earliest_force_install_date()
        if not next_logout_time:
            # no forced logout needed, so bail
            log('no forced installs found')
            time.sleep(10)  # makes launchd happy
            log('exited')
            exit(0)

        if logout_time_override is None:
            logout_time = next_logout_time
        else:
            # allow the new next_logout_time from InstallInfo to be used
            # if it has changed to a later time since when we decided to
            # override it.
            if next_logout_time > logout_time_override:
                logout_time = next_logout_time
                log('reset logout_time to: %s' % logout_time)
                logout_time_override = None
                sent_notifications = []

        # always give at least MANDATORY_NOTIFICATIONS warnings
        if logout_time < minimum_notifications_logout_time:
            for mandatory_notification in MANDATORY_NOTIFICATIONS:
                if mandatory_notification not in sent_notifications:
                    # logout time is in the past, and a mandatory notification
                    # has not been sent, so reset the logout_time to the future.
                    log('%d minute notification not sent.'
                        % mandatory_notification)
                    logout_time = NSDate.date(
                        ).addTimeInterval_(60 * mandatory_notification + 30)
                    log('reset logout_time to: %s' % logout_time)
                    logout_time_override = logout_time
                    break

        minutes_until_logout = int(logout_time.timeIntervalSinceNow() / 60)
        info_dict = {'logout_time': logout_time}
        if minutes_until_logout in NOTIFICATION_MINS:
            sent_notifications.append(minutes_until_logout)
            log('Warning user of %s minutes until forced logout'
                % minutes_until_logout)
            alert_user_of_forced_logout(info_dict)
        elif minutes_until_logout < 1:
            log('Forced logout in 60 seconds')
            alert_user_of_forced_logout(info_dict)

        time.sleep(60)
        if minutes_until_logout < 1:
            break

    if osutils.currentGUIusers() and earliest_force_install_date():
        log('Beginning forced logout')
        processes.force_logout_now()
    log('exited')
    exit(0)

if __name__ == '__main__':
    main()
