#!/usr/bin/python
#
# Copyright 2009 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
# 
#      http://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.
"""
managedsoftwareupdate
"""

import sys
import os
import optparse
import datetime
import dateutil.parser
import subprocess

from munkilib import munkicommon
from munkilib import updatecheck
from munkilib import installer
from munkilib import munkistatus
from munkilib import FoundationPlist

def getIdleSeconds():
    # stolen from Karl Kuehn -- thanks, Karl!
    # I'd like to Python-ize it a bit better; calling awk seems unPythonic, but it works.
    commandString = "/usr/sbin/ioreg -c IOHIDSystem -d 4 | /usr/bin/awk '/Idle/ { print $4 }'"
    ioregProcess = subprocess.Popen([commandString], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if ioregProcess.wait() != 0:
        return 0
    return int(int(ioregProcess.stdout.read()) / 1000000000) # convert from Nanoseconds
    
    
def clearLastNotifiedDate():
    try:
        pl = FoundationPlist.readPlist("/Library/Preferences/ManagedInstalls.plist")
        if pl:
            if 'LastNotifiedDate' in pl:
                cmd = ['/usr/bin/defaults', 'delete', '/Library/Preferences/ManagedInstalls', 
                        'LastNotifiedDate']
                retcode = subprocess.call(cmd)
    except:
        pass
        
        
def createDirsIfNeeded(dirlist):
    for directory in dirlist:
        if not os.path.exists(directory):
            try:
                os.mkdir(directory)
            except:
                print >>sys.stderr, "Could not create %s" % directory
                return False

    return True
        
        
def initMunkiDirs():
    managedinstallprefs = munkicommon.prefs()
    ManagedInstallDir = managedinstallprefs['ManagedInstallDir']
    manifestsdir = os.path.join(ManagedInstallDir, "manifests")
    catalogsdir = os.path.join(ManagedInstallDir, "catalogs")
    cachedir = os.path.join(ManagedInstallDir, "Cache")
    logdir = os.path.join(ManagedInstallDir, "Logs")
    
    if not createDirsIfNeeded([ManagedInstallDir, manifestsdir, catalogsdir, cachedir, logdir]):
        # can't use logerror function since logdir might not exist yet
        errormessage = "Could not create needed directories in %s" % ManagedInstallDir
        print >>sys.stderr, errormessage
        munkicommon.errors = errormessage
        return False
    return True


def main():
    
    # check to see if we're root
    if os.geteuid() != 0:
        print >>sys.stderr, "You must run this as root!"
        exit(-1)
        
    # check to see if another instance of this script is running
    myname = os.path.basename(sys.argv[0])
    if munkicommon.pythonScriptRunning(myname):
        # another instance of this script is running, so we should quit
        print >>sys.stderr, "Another instance of %s is running. Exiting." % myname
        exit(0)
           
    p = optparse.OptionParser()
    p.add_option('--auto', '-a', action='store_true',
                    help='''Used by launchd LaunchAgent for scheduled runs.
                    No user feedback or intervention. All other options ignored.''')
    p.add_option('--logoutinstall', '-l', action='store_true',
                    help='Used by launchd LaunchAgent when running at the loginwindow.')
    p.add_option('--manualcheck', action='store_true',
                    help='Used by launchd LaunchAgent when checking manually.')
    p.add_option('--munkistatusoutput', '-m', action='store_true',
                    help='Uses MunkiStatus.app for progress feedback when installing.')
    p.add_option('--id', default='',
                    help='Alternate identifier for catalog retreival')
    p.add_option('--quiet', '-q', action='store_true',
                    help='''Quiet mode. Logs messages, but nothing to stdout. 
                    --verbose is ignored if --quiet is used.''')
    p.add_option('--verbose', '-v', action='count', default=1,
                    help='More verbose output. May be specified multiple times.')
    p.add_option('--checkonly', action='store_true',
                       help="Check for updates, but don't install them.")
    p.add_option('--installonly', action='store_true',
                       help='Skip checking and install any pending updates.')
    
    options, arguments = p.parse_args()
        
    if options.auto:
        # munkistatusoutput is false for checking, but true for installing
        options.munkistatusoutput = False
        options.quiet = True
        options.checkonly = False
        options.installonly = False
        
    if options.logoutinstall:
        options.munkistatusoutput = True
        options.quiet = True
        options.checkonly = False
        options.installonly = True
        # if we're running at the loginwindow, let's make sure the user triggered the update
        triggerfile = "/private/tmp/com.googlecode.munki.installatlogout"
        if os.path.exists(triggerfile):
            os.unlink(triggerfile)
        else:
            # no trigger file, so don't install
            exit(0)
            
    if options.manualcheck:
        options.munkistatusoutput = True
        options.quiet = True
        options.checkonly = True
        options.installonly = False
        
    if options.quiet:
        options.verbose = 0
                           
    if options.checkonly and options.installonly:
        print >>sys.stderr, "--checkonly and --installonly options are mutually exclusive!"
        exit(-1)
        
    # set munkicommon globals
    munkicommon.munkistatusoutput = options.munkistatusoutput
    munkicommon.verbose = options.verbose
    
    # create needed directories if necessary
    if not initMunkiDirs():
        exit(-1)
        
    if not munkicommon.verbose == 0 : 
        print "Managed Software Update Tool"
        print "Copyright 2009 The Munki Project"
        print "http://code.google.com/p/munki\n"
        
    updatesavailable = False
    if not options.installonly:
        result = updatecheck.check(id=options.id)
        
        # record last check date and result
        nowString = munkicommon.NSDateNowString()
        cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls', 
                        'LastCheckDate', '-date', nowString]
        retcode = subprocess.call(cmd)
        cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls', 
                        'LastCheckResult', '-int', str(result)]
        retcode = subprocess.call(cmd)
        
        if options.manualcheck:
            munkistatus.quit()
        
        if result > 0:
            updatesavailable = True
        
        if result == -1:
            # there were errors checking for updates.
            # let's check to see if there's a InstallInfo.plist with waiting updates from
            # an earlier successful run
            installinfo = os.path.join(munkicommon.ManagedInstallDir(), 'InstallInfo.plist')
            if os.path.exists(installinfo):
                try:
                    pl = munkicommon.readPlist(installinfo)
                    removalcount = installer.getRemovalCount(pl.get('removals',[]))
                    installcount = installer.getInstallCount(pl.get('managed_installs',[]))
                    if removalcount or installcount:
                        updatesavailable = True
                except:
                    print >>sys.stderr, "Invalid %s" % installinfo
    
    if options.auto:
        # when --auto,  munkistatusoutput is false for checking, but true for installing
       munkicommon.munkistatusoutput = True         
        
    if updatesavailable or options.installonly or options.manualcheck:
        if options.auto or options.manualcheck:
            if munkicommon.getconsoleuser() == None:
                if getIdleSeconds() > 10:
                    # no-one is logged in, and 
                    # no keyboard or mouse movement for the past 10 seconds,
                    # therefore no-one is in the middle of logging in (we hope!)
                    # so just install
                    # but first, clear the last notified date
                    # so we can get notified of new changes after this round
                    # of installs
                    clearLastNotifiedDate()
                    # now install
                    installer.run()
                    
            else:
                # someone is logged in, and we have updates.
                # if we haven't notified in a while, notify:
                lastNotifiedString = munkicommon.pref('LastNotifiedDate')
                daysBetweenNotifications = munkicommon.pref('DaysBetweenNotifications')
                nowString = munkicommon.NSDateNowString()
                now = dateutil.parser.parse(nowString)
                nextNotifyDate = now
                if lastNotifiedString:
                    lastNotifiedDate = dateutil.parser.parse(lastNotifiedString)
                    interval = datetime.timedelta(days=daysBetweenNotifications)
                    if daysBetweenNotifications > 0:
                        # we make this adjustment so a "daily" notification
                        # doesn't require 24 hours to elapse
                        interval = interval - datetime.timedelta(hours=6)
                    nextNotifyDate = lastNotifiedDate + interval
                if now >= nextNotifyDate or options.manualcheck:
                    # record current notification date
                    cmd = ['/usr/bin/defaults', 'write', '/Library/Preferences/ManagedInstalls', 
                                    'LastNotifiedDate', '-date', now.ctime()]
                    retcode = subprocess.call(cmd)
                    # notify user of available updates
                    result = munkicommon.osascript('tell application "Managed Software Update" to activate')
        else:
            if options.installonly:
                # install!
                # but first, clear the last notified date so we can
                # get notified of new changes after this round
                # of installs
                clearLastNotifiedDate()
                # now we can install
                installer.run()
            else:
                if not options.quiet:
                    print "\nRun %s --installonly to install the downloaded updates." % myname
                
    if munkicommon.munkistatusoutput:
        munkistatus.quit()
    if munkicommon.tmpdir:
        munkicommon.cleanUpTmpDir()
        
if __name__ == '__main__':
    main()
