#!/usr/bin/python
# -*- coding: UTF-8 -*-
###########################################################################
# serviceconfig.py - description                                          #
# ------------------------------                                          #
# begin     : Wed Apr 30 2003                                             #
# copyright : (C) 2003-2006 by Simon Edwards                              #
# email     : simon@simonzone.com                                         #
#                                                                         #
###########################################################################
#                                                                         #
#   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 2 of the License, or     #
#   (at your option) any later version.                                   #
#                                                                         #
###########################################################################

from qt import *
from tdeui import *
from tdecore import *
import sys
import os
import os.path
import posix
import re
from pickle import Pickler,Unpickler
import locale

programname = "Services Configuration"
version = "0.8.1"

# Holding the name of the distribution, defaults to ...
DISTRO = "Mandrake"

# These get overridden for Debian. 
initdir = "/etc/init.d"
rcdir = "/etc/rc.d"

chkconfigpath = "/sbin/chkconfig"
statusblacklist = ['iptables']

# Are we running as a separate standalone application or in KControl?
standalone = __name__=='__main__'

# Running as the root user or not?
isroot = os.getuid()==0

#print "Warning:: root check is disabled"
#isroot = True

############################################################################
class DescriptionCache(object):

    proto = 2 ## Choose latest available version of Pickler
    _lines = []
    data = {}

    def __init__(self,filename,path="/tmp"):
        if not os.path.isdir(path):
            print path, "is not a valid directory, can't cache."

        self.filename = os.path.join(path,filename)

    def loadCache(self):
        try:
            fhandle = open(self.filename, "r")
            pickler = Unpickler(fhandle)
            self.data = pickler.load()
            fhandle.close()
        except IOError, e:
            print "Couldn't load file:", e
            print "Not reading description cache data"

    def saveCache(self):
        """ Save pickled dataobject to the file. We don't want the user to be able to save a datastructure that root 
            will read in since that would have implications for security."""
        if not isroot:
            print "Not saving description cache data for security reasons, we're not root"
            return

        try:
            fhandle = open(self.filename, "w")
            pickler = Pickler(fhandle, self.proto) 
            pickler.dump(self.data) 
            fhandle.close()
            #print "Saved description cache data to ", self.filename
        except IOError, e:
            print "Can't cache:", e

    def add(self,filename,packagename,description):
        self.data[filename] = (packagename,description)

    def readDescription(self,filename):
        """ Tries to look up the package description, returns None if it can't find it."""
        try:
            return self.data[filename][1]
        except KeyError:
            return None

    def readPackagename(self,filename):
        """ Tries to look up the Packagename, returns None if it can't find it."""
        try:
            return self.data[filename][0]
        except KeyError:
            return None

############################################################################
# Holds all of the info about a single service.
class Service(object):
    ########################################################################
    def __init__(self,context,filename):
        self.context = context
        self.filename = filename
        self.longname = ""
        self.description = ""
        self.runlevels = []
        self.gotStatus = False
        self.statusable = False
        self.status = unicode(i18n("?"))
        self.startpriority = "50"
        self.killpriority = "50"

        try:
            fhandle = open(os.path.join(initdir,self.filename))
            place = 0
            for line in fhandle.readlines():
                line = line.strip()
                if place==0:
                    if line.startswith("#!/bin/sh") or line.startswith("#! /bin/sh") or \
                            line.startswith("#!/bin/bash") or line.startswith("#! /bin/bash"):
                        place = 1
                elif place==1:  # Grab the short description line.
                    if line!="#":
                        if line.startswith("# chkconfig:"):
                            parts = line[12:].split()
                            if len(parts)>=1:
                                for c in parts[0]:
                                    try:
                                        rl = self.context.getRunLevelByNumber(int(c))
                                        if rl is not None:
                                            self.runlevels.append(rl)
                                            rl.availableservices.append(self)
                                    except ValueError:
                                        pass
                            if len(parts)>=2:
                                self.startpriority = parts[1]
                            if len(parts)>=3:
                                self.killpriority = parts[2]
                        else:
                            self.longname = line[2:]
                        place = 2
                elif place==2:  # Look for the description line
                    if line.startswith("# chkconfig:"):
                        parts = line[12:].split()
                        if len(parts)>=1:
                            for c in parts[0]:
                                try:
                                    rl = self.context.getRunLevelByNumber(int(c))
                                    if rl is not None:
                                        self.runlevels.append(rl)
                                        rl.availableservices.append(self)
                                except ValueError:
                                    pass
                        if len(parts)>=2:
                            self.startpriority = parts[1]
                        if len(parts)>=3:
                            self.killpriority = parts[2]
                    elif line.startswith("# description:"):
                        self.description = line[15:]
                        if self.description[-1:]=="\\":
                            self.description = self.description[:-1]
                            place = 3
                        else:
                            place = 2
                elif place==3:  # Grab a description continuation line.
                    if line[0]=="#":
                        self.description += line[1:].strip()
                        if self.description[-1:]=="\\":
                            self.description = self.description[:-1]
                        else:
                            place = 2
                if line.startswith("status)"):
                    self.statusable = True

            fhandle.close()
            if self.filename in statusblacklist:
                self.statusable = False
        except IOError:
            pass

        if self.longname=="":
            self.longname = self.filename

        if len(self.runlevels)==0:
            self.runlevels = self.context.runlevels[:]
            for rl in self.context.runlevels:
                rl.availableservices.append(self)

    ########################################################################
    def isAvailableInRunlevel(self,level):
        return level in self.runlevels

    ########################################################################
    def fetchStatus(self):
        global initdir
        if self.statusable:
            self.status = os.popen(os.path.join(initdir,self.filename) + " status").read()
        self.gotStatus = True

    ########################################################################
    def doStartCommand(self):
        self.gotStatus = False
        return "export CONSOLETYPE=serial && " + os.path.join(self.context.initdir,self.filename) + " start"

    ########################################################################
    def doStopCommand(self):
        self.gotStatus = False
        return "export CONSOLETYPE=serial && " + os.path.join(self.context.initdir,self.filename) + " stop"

    ########################################################################
    def doRestartCommand(self):
        self.gotStatus = False
        return "export CONSOLETYPE=serial && " + os.path.join(self.context.initdir,self.filename) + " restart"

############################################################################
class ServiceContext(object):
    def __init__(self):
        self.runlevels = []
        self.services = {}

    def loadInfo(self): pass
    def currentRunLevel(self): pass
    def newRunLevel(self): pass
    def newService(self,file): pass
    def getRunLevelByNumber(self,num):
        for rl in self.runlevels:
            if rl.levelnum==num:
                return rl
        return None

############################################################################
def getServiceContext():
    global DISTRO
    # Detect if we are running on Debian, Mandrake or what.

    # Uncomment here to test Gentoo compatibility.
    #DISTRO = "Gentoo"; return GentooServiceContext()

    # Check for Debian - is this the 'good' way? 
    etc_issue = '/etc/issue'

    # Knoppix and Kanotix have a symlink called /etc/issue
    if os.path.islink(etc_issue):
        etc_issue = posix.readlink(etc_issue)

    if os.path.isfile(etc_issue):
        etc_issue = open(etc_issue)
        system_name = etc_issue.readline()
        etc_issue.close()
        if system_name.startswith("Debian") or system_name.startswith("Ubuntu"):
            DISTRO = "Debian"
            return DebianServiceContext()

    # Might this be Gentoo Linux?
    if os.path.isfile('/etc/gentoo-release'):
        DISTRO = "Gentoo"
        return GentooServiceContext()

    # Mandrake is default.
    return MandrakeServiceContext()

############################################################################
class MandrakeServiceContext(ServiceContext):
    ########################################################################
    def __init__(self):
        ServiceContext.__init__(self)
        self.initdir = "/etc/init.d"
        self.rcdir = "/etc/rc.d"
        self.runlevels.append(SysVRunLevel(self,0,i18n("Halt (0)")))
        self.runlevels.append(SysVRunLevel(self,1,i18n("Single User Mode (1)")))
        self.runlevels.append(SysVRunLevel(self,2,i18n("Multiuser mode with Networking (2)")))
        self.runlevels.append(SysVRunLevel(self,3,i18n("Multiuser mode (3)")))
        self.__currentrunlevel =  SysVRunLevel(self,5,i18n("GUI Multiuser mode (5)"))
        self.runlevels.append(self.__currentrunlevel)
        self.runlevels.append(SysVRunLevel(self,6,i18n("Reboot (6)")))
        self._filenametoservice = {}

    ########################################################################
    def currentRunLevel(self):
        return self.__currentrunlevel

    ########################################################################
    def loadInfo(self):
        # Load in all of the service info.
        for filename in os.listdir(self.initdir):
            newservice = self.newService(filename)
            self.services[filename] = newservice
            self._filenametoservice[filename] = newservice

        # Now load in which services are active in which run level.
        for rl in self.runlevels:
            rl.loadInfo()

    ########################################################################
    def newService(self,file):
        return Service(self,file)

############################################################################
class SysVRunLevel(object):
    ########################################################################
    def __init__(self,context,levelnum,name):
        self.context = context
        self.levelnum = levelnum
        self.name = name
        self.availableservices = []
        self.activeservices = []
        self.leveldir = os.path.join(self.context.rcdir,"rc"+str(self.levelnum)+".d")

    ########################################################################
    def loadInfo(self):
        #print "SysVRunLevel(object).loadInfo",self.leveldir
        for filename in os.listdir(self.leveldir):
            if filename.startswith("S") and os.path.islink(self.leveldir+"/"+filename):
                target = os.path.basename(posix.readlink(self.leveldir+"/"+filename))
                if target in self.context._filenametoservice:
                    self.activeservices.append(self.context._filenametoservice[target])
                #else:
                #    print "Couldn't find service '%s' in runlevel %i." % (target, self.levelnum)

    ########################################################################
    def isActiveAtBoot(self,service):
        return service in self.activeservices

    ########################################################################
    def setActiveAtBoot(self,service,activeflag):
        leveldir = self.leveldir

        # Remove any existing links to the service script
        for filename in os.listdir(leveldir):
            link = os.path.join(leveldir,filename)
            if os.path.islink(link):
                target = os.path.basename(posix.readlink(os.path.join(leveldir,filename)))
                if target==service.filename:
                    # Kill target.
                    #print "Killing link",link
                    os.remove(link)

        if activeflag:
            #print "symlink(",leveldir+"/S"+service.startpriority+service.filename,",",self.context.initdir+"/"+service.filename,")"
            posix.symlink( os.path.join(self.context.initdir,service.filename),
                os.path.join(leveldir,"S"+service.startpriority+service.filename))
            self.activeservices.append(service)
        else:
            #print "symlink(", leveldir+"/K"+service.killpriority+service.filename,",",self.context.initdir+"/"+service.filename,")"
            posix.symlink( os.path.join(self.context.initdir,service.filename),
                os.path.join(leveldir,"K"+service.killpriority+service.filename))
            self.activeservices.remove(service)

############################################################################
#
# Here's the Debian specific stuff, a Service, a ServiceContext and a RunLevel
# 
# Enjoy. ;-)

class DebianService(Service):
    """ Mapping for services which don't use a pidfile like /var/run/<service>.pid
        Services not in here are lookup up "normally" """
    pidfiles = {    'acpid':'acpid.socket',
                    'spamassassin':'spamd.pid',
                    'dbus':'dbus/pid',
                    'klogd':'klogd/klogd.pid',
                    'samba':'samba/smbd.pid',
                    'zope':'zope/default/Z2.pid',
                    'mysql':'mysqld/mysqld.pid',
                    'hald':'hal/hald.pid',
                    'sysklogd':'syslogd.pid',
                    'ssh':'sshd.pid',
                    'cron':'crond.pid',
                    'slapd':'slapd/slapd.pid',
                    'laptop-mode':'laptop-mode-enabled',
                    'cupsys':'cups/cupsd.pid'
                }

    ########################################################################
    def __init__(self,context,filename):
        self.context = context
        self.filename = filename
        self.path_and_filename = os.path.join(context.initdir, self.filename)
        self.packagename = False
        self.description = i18n("Description is being loaded.")
        self.runlevels = []
        self.gotStatus = False
        self.statusable = False
        self.status = unicode(i18n("not running"))
        self.startpriority = "50"
        self.killpriority = "50"
        self.getStatusFrom = "pidfile"

        self.fetchStatus()    

        if len(self.runlevels)==0:
            self.runlevels = self.context.runlevels[:]
            for rl in self.context.runlevels:
                rl.availableservices.append(self)

    def fetchDescription(self):
        self.description = self.context.descriptioncache.readDescription(self.filename)
        self.packagename = self.context.descriptioncache.readPackagename(self.filename)
        if not self.description:
            if not self.packagename:
                self.fetchPackageName()
                #print "     packagename", self.packagename
            if self.packagename:
                # FIXME: don't assume english output!
                command = "apt-cache show " + self.packagename

                self.description = ""
                description_label = "Description:"
                for line in os.popen(command).readlines():
                    if line.startswith(description_label):
                        self.description = line.strip()[len(description_label):]

                self.context.descriptioncache.add(self.filename, self.packagename, self.description, )
            else:
                self.description = i18n("Couldn't fetch a description from apt.")

    def fetchPackageName(self):
        if os.path.isfile(self.path_and_filename):
            command = "dpkg -S " + self.path_and_filename
            self.packagename = None # as opposed to False( = not yet fetched)
            for line in os.popen(command).readlines():
                if ":" in line:
                    self.packagename = line.strip().split(":")[0]
        else:
            print self.path_and_filename + " is no file or does not exist!"

    ########################################################################
    def fetchStatus(self):
        if self.getStatusFrom == "pidfile":
            self.fetchStatusFromPidFile()
        elif self.getStatusFrom == "top":
            # FIXME: not yet implemented
            self.fetchStatusFromTop()

    ########################################################################
    def fetchStatusFromTop(self):
        # FIXME, incomplete.
        top = os.popen("ps -aux")

    ########################################################################
    def fetchStatusFromPidFile(self):
        try:
            if os.path.isfile(os.path.join('/var/run',self.pidfiles[self.filename])):
                self.status = unicode(i18n("running"))
            else:
                self.status = unicode(i18n("not running"))
        except KeyError:
            if os.path.isfile(os.path.join('/var/run',self.filename + '.pid')):
                self.status = unicode(i18n("running"))
            else:
                self.status = unicode(i18n("not running"))
        self.gotStatus = True

############################################################################
class DebianServiceContext(ServiceContext):
    """ bootscripts are scripts that are only running once at boot and where starting, 
    stopping and restarting does not really make sense, generally exclude these from 
    serviceconfig list."""
    bootscripts = ( 'README',
                    'acpi-support',
                    'xorg-common',
                    'binfmt-support',
                    'bootclean.sh',
                    'bootmisc.sh',
                    'checkfs.sh',
                    'checkroot.sh',
                    'console-screen.sh',
                    'dns-clean',
                    'glibc.sh',
                    'halt',
                    'hostname.sh',
                    'hwclock.sh',
                    'hwclockfirst.sh',
                    'initrd-tools.sh',
                    'keymap.sh',
                    'makedev',
                    'module-init-tools',
                    'mountall.sh',
                    'mountvirtfs',
                    'mountnfs.sh',
                    'nvidia-kernel',
                    'procps.sh',
                    'pppd-dns',
                    'powernowd.early',
                    'rc',
                    'rc.local',
                    'rcS',
                    'readahead',
                    'readahead-desktop',
                    'reboot',
                    'rmnologin',
                    'screen-cleanup',
                    'screen',
                    'sendsigs',
                    'single',
                    'skeleton',
                    'stop-bootlogd',
                    'stop-readahead',
                    'umountfs',
                    'umountnfs.sh',
                    'urandom'
            )

    def __init__(self):
        """
        Debian uses the following runlevels: 

        1 (single-user mode), 
        2 through 5 (multiuser modes), and 
        0 (halt the system), 
        6 (reboot the system). 

        Runlevels 7, 8, and 9 can also be used but their rc directories are not populated 
        when packages are installed. They are intentionally left out here, but should be 
        easy to add.
        """
        ServiceContext.__init__(self)

        self.initdir = "/etc/init.d"
        self.rcdir = "/etc"
        self.relative_initdir = "../init.d"

        deb_runlevels = {   0 : i18n("Halt (0)"), 
                            1 : i18n("Single User Mode (1)"),
                            "S" : i18n("Single User Mode (S)"),
                            2 : i18n("Multiuser Mode (2)"),
                            3 : i18n("Multiuser Mode (3)"),
                            4 : i18n("Multiuser Mode (4)"),
                            5 : i18n("Multiuser Mode (5)"),
                            6 : i18n("Reboot (6)") }

        # Lookup what runlevel we're in.
        shell_output = os.popen('/sbin/runlevel')
        raw_runlevel = shell_output.readline()
        shell_output.close()
        cur_runlevel = raw_runlevel[2:-1]

        for num in deb_runlevels.keys():
            if cur_runlevel.isdigit():
                if num == int(cur_runlevel):
                    self.__currentrunlevel =  DebianRunLevel(self, num, deb_runlevels[num])
                    self.runlevels.append(self.__currentrunlevel)
                else:
                    self.runlevels.append(DebianRunLevel(self, num, deb_runlevels[num]))
            else:
		if num == cur_runlevel:
		    self.__currentrunlevel =  DebianRunLevel(self, num, deb_runlevels[num])
                    self.runlevels.append(self.__currentrunlevel)
		else:
		    self.runlevels.append(DebianRunLevel(self, num, deb_runlevels[num]))
        self._filenametoservice = {}

    ########################################################################
    def currentRunLevel(self):
        return self.__currentrunlevel

    ########################################################################
    def loadInfo(self):
        # Load in all of the service info.
        initscripts = os.listdir(self.initdir)
        # Remove "bootscripts" from our list.
        servicefiles = []
        self.services = []
        for script in initscripts:
            if script not in self.bootscripts:
                try:
                    # Exclude backup copies.
                    if script.split(".")[1] not in ("orig","dpkg-dist"):
                        servicefiles.append(script)
                except IndexError:
                    servicefiles.append(script)

        for filename in servicefiles:
            if filename not in self.bootscripts:
                newservice = self.newService(filename)
                self.services.append(newservice)
                self._filenametoservice[filename] = newservice

        # Now load in which services are active in which run level.
        for rl in self.runlevels:
            rl.loadInfo()

    ########################################################################
    def newService(self,file):
        return DebianService(self,file)

############################################################################
class DebianRunLevel(SysVRunLevel):
    ########################################################################
    def setActiveAtBoot(self,service,activeflag):
        """ Adds a Service to a runlevel.

        Activating a service adds start symlinks in the respective levels, and 
        maintains symlinks in levels 0, 1 and 6 (halt, single user and reboot).
        """
        leveldir = self.context.rcdir

        def createSymlink(target, linkname):
            """ Creates a symlink after having checked if it makes sense to do so.
            We first change to the rcdir, then create a relative symlink and then 
            change back, sounds weird, but Debian's own scripts break when the 
            symlinks are not relative.

            Returns True or False and prints debugging message.
            """
            odir = os.getcwd()
            tmpdir = "/".join(linkname.split("/")[0:-1]) # FIXME use os.path
            os.chdir(tmpdir)

            if not os.path.isfile(target) or os.path.islink(target):
                #print target + " is not a valid filename. Can't create symlink."
                os.chdir(odir)
                return False

            if os.path.islink(linkname) and posix.readlink(linkname) == target:
                #print "Symlink " + linkname + " -> " + target + " already exists."
                os.chdir(odir)
                return True

            if os.path.islink(linkname) and posix.readlink(linkname) != target:
                #print "Removing symlink, " + linkname + ", the target does not match."
                try:
                    posix.unlink(linkname)
                except OSError, e:
                    print "Couldn't remove symlink " + linkname + " :: " + str(e)

            try:
                posix.symlink(target, linkname)
                #print "Created symlink " + linkname + " -> " + target + " successfully."
                os.chdir(odir)
                return True
            except OSError, e:
                #print "Creating symlink " + linkname + " -> " + target + " failed: " + str(e)
                os.chdir(odir)
                return False

        def removeSymlink(servicename, runleveldir, KorS):
            if KorS not in ('K','S'): 
                print "OUCH, symlinks have to start with S or K!"
                return
            for link in os.listdir(runleveldir):
                if (link[0] == KorS) and (link[3:] == servicename):
                    #print "Killing ...", runleveldir+link
                    posix.unlink(os.path.join(runleveldir,link))

        # In these levels, the K symlinks are created.                
        stop_levels = (0,1,6)                        
        l_num = str(self.levelnum)

        if activeflag:
            target = os.path.join(self.context.relative_initdir,service.filename)
            createSymlink(target, os.path.join(self.context.rcdir,"rc"+l_num+".d","S"+service.startpriority+service.filename))
            # Kill links:
            for i in stop_levels: 
                createSymlink(target, os.path.join(self.context.rcdir,"rc"+str(i)+".d","K"+service.killpriority+service.filename))
            self.activeservices.append(service)
        else:

            try:
                s_link = os.path.join(leveldir,"rc"+l_num+".d","S"+service.startpriority+service.filename)
                runleveldir = os.path.join(leveldir,"rc"+l_num+".d")
                #print "Removing symlink " + s_link 
                removeSymlink(service.filename, runleveldir, "S")
            except OSError, e:
                print "Could not remove symlink " + s_link + " :: " + str(e)

            self.activeservices.remove(service)

            # check if service has to be started in other runlevels:
            #    Y: Don't touch links
            #    N: Remove symlinks

            #print "Should remove symlinks here."
            for rl in self.context.runlevels:
                if service in rl.activeservices:
                    #print "Service " + service.filename + " is still used in runlevel " + \
                    #    str(rl.levelnum) + ", not removing K-Links."
                    break
            else:
                # The service is not being used anywhere. We can remove it now.

                #print "Service completely inactive, removing K-links."
                for i in stop_levels:
                    k_link = os.path.join(leveldir,"rc"+str(i)+".d","K"+service.killpriority+service.filename)
                    runleveldir = os.path.join(leveldir,"rc"+str(i)+".d")

                    try:
                        #print "Removing " + k_link
                        removeSymlink(service.filename, runleveldir, "K")
                    except OSError, e:
                        print "Could not remove " + k_link + " :: " + str(e)


############################################################################
#
# Here come all the Gentoo specific pieces.
# Gentoo Linux has a special way of organizing the init and runlevels stuff.

class GentooService(DebianService):
    """ GentooService

    Services in Gentoo are handled very much like the ones in Debian, except
    that there is rc-status to check whether a service is running or not.
    """
    ########################################################################
    def fetchStatus(self):
        # rc-status is run everytime we check a service, this might be some
        # more efficiently solved.
        # FIXME: add check if 'rc-status' is in current PATH
        rc_status_fhandle = os.popen('rc-status')
        for line in rc_status_fhandle.readlines():
            parts = line.split()
            if parts[0] == self.filename:
                # Who needs Perl? ;-)
                # FIXME: set the terminal type to serial when running rc-status.
                self.status = line.split(';01m')[2].split('\x1b[')[0].strip()
        rc_status_fhandle.close()
        self.gotStatus = True

    def fetchDescription(self):
        # Temporary vars.
        description_lines = []
        first_block = True

        if os.path.isfile(self.path_and_filename):
            fhandle = open(self.path_and_filename)
            for line in fhandle.readlines():
                # Ignore blank lines and CVS Headers:
                if len(line.strip()) > 1 and line[:2] != '#!' and line[:10] != '# $Header:':
                    # Cut off newline at the end.
                    line = line[:-1]
                    # The first commencted block might be the description.
                    if first_block:
                        if line[0] != '#': first_block = False
                        else: description_lines.append(line[1:].strip())
            fhandle.close()
        else:
            print self.path_and_filename + " is no file or does not exist!"

        if len(description_lines):
            self.description = "\n".join(description_lines)
        else:
            self.description = i18n("Could not extract description.")

    ########################################################################
    def doZapCommand(self):
        self.gotStatus = False
        return "export CONSOLETYPE=serial && rc-status"
        return "export CONSOLETYPE=serial && "+os.path.join(self.context.initdir,self.filename)+" zap"

############################################################################
class GentooServiceContext(ServiceContext):
    ########################################################################
    def __init__(self):
        """
        Gentoo uses customized runlevels, see the Gentoo Documentation for 
        the dirty details. The runlevels are defined in inittab, so we have 
        a look there. 

        - Runlevel links reside in /etc/runlevels/${RUNLEVEL}/.
        - Default existing runlevels are boot/, nonetwork/ and default/.
        - Custom runlevels and default are defined in /etc/inittab.

        Dependencies between runscripts / initscrips are not handled here, 
        this is responsibility of rc-update. Also, after rc-update has run,
        some items might be necessary to refresh.

        """
        ServiceContext.__init__(self)

        # Here comes the Gentoo specific stuff.
        # First off, parsing inittab for runlevels available.
        def parseInittab():
            if os.path.isfile(self.inittab):
                inittab_fhandle = open(self.inittab, 'r')
                rl_appended =  []
                for line in inittab_fhandle.readlines():
                    line = line[:-1]
                    # Ignore blank and commented lines.
                    if len(line.strip()) > 1 and line.strip()[0] != '#':
                        parts = line.split(':')
                        if len(parts) == 4 and parts[2] == 'wait':
                            rl_num, rl_label = parts[1], parts[3].split()[1] +' ('+parts[1]+')'
                            rl_name = parts[3].split()[1]
                            #print "Num: " + rl_num + " Label: " + rl_label + " Name: " + parts[3].split()[1]
                            # This is a runlevel in Gentoo, is it the current one?
                            if parts[1] == self.current_runlevelnum:
                                self.__currentrunlevel = GentooRunLevel(self, rl_num, rl_name, rl_label)
                                self.runlevels.append(self.__currentrunlevel)
                                rl_appended.append(rl_name)
                            else:
                                if rl_name not in rl_appended:
                                    self.runlevels.append(GentooRunLevel(self, rl_num, rl_name, rl_label))
                                    rl_appended.append(rl_name)
                        elif len(parts) == 4 and parts[2] == 'bootwait':
                            # The boot runlevel does not have a 'real' runlevel number, so we use 0.
                            self.runlevels.append(GentooRunLevel(self, 0, parts[3].split()[1], 'boot'))
                            rl_appended.append('boot')
                    inittab_fhandle.close()

        def currentRunLevelNum():
            runlevelbin = "/sbin/runlevel"
            if not os.path.isfile(runlevelbin):
                print "Couldn't find %s, that sucks. :o" % runlevelbin
                sys.exit(1)
            shell_output = os.popen(runlevelbin)
            raw_runlevel = shell_output.readline()
            shell_output.close()
            return raw_runlevel[2:-1]

        self.initdir = "/etc/init.d"
        self.rcdir = "/etc/runlevels"
        self.inittab = "/etc/inittab"

        #self.initdir = "/home/sebas/gentooinit/init.d"
        #self.rcdir = "/home/sebas/gentooinit/runlevels"
        #self.inittab = "/home/sebas/gentooinit/inittab"

        self.current_runlevelnum = currentRunLevelNum()
        parseInittab()
        self._filenametoservice = {}

    ########################################################################
    def currentRunLevel(self):
        return self.__currentrunlevel

    ########################################################################
    def loadInfo(self):
        """ Load in all of the service info for every file in the init.d directory. """
        for filename in os.listdir(self.initdir):
            # Exclude backup files from portage and .sh files like shutdown, depscan, etc.
            if (filename.find('._cfg')<0)  and not filename.endswith(".sh"):
                newservice = self.newService(filename)
                self.services[filename] = newservice
                self._filenametoservice[filename] = newservice

        # Now load in which services are active in which run level.
        for rl in self.runlevels:
            rl.loadInfo()

    ########################################################################
    def newService(self,file):
        return GentooService(self,file)


############################################################################
class GentooRunLevel(SysVRunLevel):
    """ Gentoo Runlevel

        GentooRunLevel has an additional parameter. the 'label' gets displayed in the 
        'Run level:' drop-down menu, the 'name' is used. 'name' is internally handled 
        in very much the same way as SysVRunLevel.levelnum. It corresponds to the 
        actual runlevels Gentoo uses, such as /etc/runlevels/default/, rather than 
        using SysVRunLevel.levelnum, as it would be in a SysV init. """

    ########################################################################
    def __init__(self,context,levelnum,dirname,label):
        SysVRunLevel.__init__(self,context,levelnum,label)

        self.dirname = dirname

        self.leveldir = self.context.rcdir+'/'+self.dirname
        # Not all runlevels in Gentoo correspond to a runlevel directory.
        self.no_dirs = []
        if not os.path.isdir(self.leveldir): 
            #self.no_dirs = ('reboot', 'shutdown', 'single')
            if self.dirname not in self.no_dirs: 
                self.no_dirs.append(self.dirname)
                print "Runlevel " + self.leveldir + " is not a valid path. '" +  self.dirname + "'"
            self.leveldir = False
            return

    ########################################################################
    def loadInfo(self):
        """ Only look up active services if runlevel path exists, else leave empty. """
        if self.leveldir:
            print "GentooRunLevel.loadInfo() from " + self.leveldir
            for filename in os.listdir(self.leveldir):
                # Exclude backup files from portage and .sh files like shutdown, depscan, etc.
                if (filename.find('._cfg')<0) and not filename.endswith('.sh'):
                    linkname = self.leveldir+"/"+filename
                    if os.path.islink(linkname):
                        target = os.path.basename(posix.readlink(linkname))
                        if target in self.context._filenametoservice:
                            self.activeservices.append(self.context._filenametoservice[target])
                        else:
                            print "Couldn't find service '%s'. " % target
                    else:
                        print "%s is not a valid symlink." % linkname

    ########################################################################
    def setActiveAtBoot(self,service,activeflag):
        """ Runs rc-update to add and remove Services from RunLevels.

            Dependencies are checked from within runscript.sh, so we don't handle them here. """

        # FIXME :: "Start at Boot" column does not properly get updated once it's "False".
        # The commands issued might better be passed through via CommandRunner.
        if self.name in self.no_dirs:
            print "Runlevel has no corresponding path, running rc-update anyway."
        if activeflag:
            if not service in self.activeservices:
                rc_add_cmd = "rc-update add %s %s" % (service.filename, self.dirname) 
                print rc_add_cmd
                # The brave really run it yet.
                os.system(rc_add_cmd)
                self.activeservices.append(service)
        else:
            if service in self.activeservices:
                rc_del_cmd = "rc-update del %s %s" % (service.filename, self.dirname)
                print rc_del_cmd
                # The brave really run it yet.
                os.system(rc_dell_cmd)
                self.activeservices.remove(service)

############################################################################

# Try translating this code to C++. I dare ya!
if standalone:
    programbase = KDialogBase
else:
    programbase = TDECModule

# is_shown exists to prevent loadDescriptions from running two times, which is 
# the case when we're running inside kcontrol. Yes, this is an ugly hack. :(
# It's set to True after show has finished once. It doesn't play a role when
# we're running standalone.
is_shown = False

class SysVInitApp(programbase):
    ########################################################################

    def __init__(self,parent=None,name=None):
        global standalone,isroot, DISTRO, is_shown
        TDEGlobal.locale().insertCatalogue("guidance")

        if standalone:
            KDialogBase.__init__(self,KJanusWidget.Plain,i18n("Service Configuration"), \
                KDialogBase.User1|KDialogBase.Close, KDialogBase.Close)
            self.setButtonText(KDialogBase.User1,i18n("About"))
        else:
            TDECModule.__init__(self,parent,name)
            self.setButtons(1)
            self.aboutdata = MakeAboutData()

        # Create a configuration object.
        self.config = TDEConfig("serviceconfigrc")
        TDEGlobal.iconLoader().addAppDir("guidance")

        self.updatingGUI = False
        self.context = getServiceContext()
        self.servicestolistitems = {}   # Map service names to QListViewItems
        self.currentrunlevel = self.context.currentRunLevel()

        self.context.loadInfo()

        self.aboutus = TDEAboutApplication(self)

        if standalone:
            toplayout = QVBoxLayout( self.plainPage(), 0, KDialog.spacingHint() )
            tophb = QSplitter(Qt.Horizontal, self.plainPage())
        else:
            toplayout = QVBoxLayout( self, 0, KDialog.spacingHint() )
            tophb = QSplitter(Qt.Horizontal, self)

        toplayout.addWidget(tophb)

        vb = QVBox(tophb)
        vb.setSpacing(KDialog.spacingHint())

        hb = QHBox(vb)
        hb.setSpacing(KDialog.spacingHint())
        vb.setStretchFactor(hb,0)

        label = QLabel(hb)
        label.setPixmap(UserIcon("hi32-app-daemons"))
        hb.setStretchFactor(label,0)

        label = QLabel(i18n("Run level:"),hb)
        hb.setStretchFactor(label,0)
        self.runlevelcombo = QComboBox(hb)

        # Load up the runlevel combo box.
        i = 0
        for runlevel in self.context.runlevels:
            self.runlevelcombo.insertItem(runlevel.name)
            if self.context.currentRunLevel() is runlevel:
                self.runlevelcombo.setCurrentItem(i)
            i += 1

        hb.setStretchFactor(self.runlevelcombo,0)

        self.connect(self.runlevelcombo, SIGNAL("activated(int)"), self.slotRunLevelChanged)

        widget = QWidget(hb)
        hb.setStretchFactor(widget,1)

        self.servicelistview = TDEListView(vb)
        self.servicelistview.addColumn(i18n("Service"))
        self.servicelistview.addColumn(i18n("Start at Boot"))
        self.servicelistview.addColumn(i18n("Status"))
        self.servicelistview.setAllColumnsShowFocus(True)
        self.servicelistview.setSelectionMode(QListView.Single)
        self.connect(self.servicelistview, SIGNAL("selectionChanged(QListViewItem *)"), self.slotListClicked)

        # Right hand side of the dialog.
        vb = QVBox(tophb)
        vb.setSpacing(KDialog.spacingHint())

        hgb = QHGroupBox(i18n("Service Details"),vb)
        vb.setStretchFactor(hgb,1)
        vb2 = QVBox(hgb)
        vb2.setSpacing(KDialog.spacingHint())

        label = QLabel(i18n("Description:"),vb2)
        vb2.setStretchFactor(label,0)
        self.descriptiontextedit = QTextEdit(vb2)
        vb2.setStretchFactor(self.descriptiontextedit,2)
        self.descriptiontextedit.setReadOnly(True)
        self.startatbootcheckbox = QCheckBox(i18n("Start during boot"),vb2)
        vb2.setStretchFactor(self.startatbootcheckbox,0)
        self.connect(self.startatbootcheckbox, SIGNAL("toggled(bool)"), self.slotBootChanged)

        label = QLabel(i18n("Status:"),vb2)
        vb2.setStretchFactor(label,0)
        self.statustext = QTextEdit(vb2)
        self.statustext.setReadOnly(True)
        vb2.setStretchFactor(self.statustext,1)

        hb2 = QHBox(vb2)
        hb2.setSpacing(KDialog.spacingHint())
        vb2.setStretchFactor(hb2,0)
        self.startbutton = QPushButton(i18n("Start"),hb2)
        hb2.setStretchFactor(self.startbutton,1)
        self.connect(self.startbutton, SIGNAL("clicked()"), self.slotStartButton)
        self.stopbutton = QPushButton(i18n("Stop"),hb2)
        hb2.setStretchFactor(self.stopbutton,1)
        self.connect(self.stopbutton, SIGNAL("clicked()"), self.slotStopButton)
        self.restartbutton = QPushButton(i18n("Restart"),hb2)
        hb2.setStretchFactor(self.restartbutton,1)
        self.connect(self.restartbutton, SIGNAL("clicked()"), self.slotRestartButton)

        if DISTRO == "Gentoo":
            # Gentoo Linux gets an extra button.
            self.zapbutton = QPushButton(i18n("Zap"),hb2)
            hb2.setStretchFactor(self.zapbutton,1)
            self.connect(self.zapbutton, SIGNAL("clicked()"), self.slotZapButton)

        if not isroot:
            self.disableStuff()
        else:
            self.connect(self.servicelistview, SIGNAL("contextMenu(TDEListView*,QListViewItem*,const QPoint&)"), 
                    self.slotServiceContextMenu)

        self.__fillListView(self.currentrunlevel)
        self.__selectFirstService()

        self.cr = CommandRunner(None,"title")
        self.timerid = None

    ########################################################################
    def __del__(self):
        pass

    def disableStuff(self):
        """Disable a couple of widgets when not running as root"""
        self.startatbootcheckbox.setDisabled(True)
        self.startbutton.setDisabled(True)
        self.restartbutton.setDisabled(True)
        self.stopbutton.setDisabled(True)
        if DISTRO == "Gentoo":
            self.zapbutton.setDisabled(True)

    def slotServiceContextMenu(self,l,v,p):
        self.cmenu = TDEPopupMenu(self,"MyActions")
        self.cmenu.insertItem(i18n("Start..."), self.slotStartButton)
        self.cmenu.insertItem(i18n("Stop..."), self.slotStopButton)
        self.cmenu.insertItem(i18n("Restart..."), self.slotRestartButton)
        self.cmenu.insertItem(i18n("Toggle start during boot..."), self.slotBootChangedAndToggle)

        self.cmenu.exec_loop(p)

    def slotBootChangedAndToggle(self):
        """Wrap slotBootChanged in order to pass the status of the checkbox, used from contextmenu."""
        self.startatbootcheckbox.toggle()

    ########################################################################
    # KDialogBase method
    def exec_loop(self):
        global programbase

        self.__loadOptions()
        programbase.exec_loop(self)
        self.__saveOptions()

    ########################################################################
    def __selectFirstService(self):                
        # Grab the first service in the list and select it.
        services = self.currentrunlevel.availableservices[:]
        services.sort(lambda x,y: cmp(x.filename,y.filename))
        self.selectedservice = None
        try:
            self.selectedservice = services[0]
            lvi = self.servicestolistitems[self.selectedservice]
            self.servicelistview.setSelected(lvi,True)
        except IndexError:
            pass
        self.__selectService(self.selectedservice)

    ########################################################################
    def show(self):
        global standalone,isroot, is_shown

        programbase.show(self)
        self.updatingGUI = True
        if isroot:
            self.__selectFirstService()
        self.updatingGUI = False
        self.__checkServiceStatus()

        if DISTRO == "Debian" and (standalone or (not standalone and is_shown)):
            QTimer.singleShot(0,self.__startLoadDescriptions)
        is_shown = True


    ########################################################################
    # KDialogBase method
    def slotUser1(self):
        self.aboutus.show()

    ########################################################################
    def __fillListView(self,runlevelobj):
        self.servicelistview.clear()
        self.servicestolistitems = {}

        services = self.currentrunlevel.availableservices[:]
        services.sort(lambda x,y: cmp(x.filename,y.filename))

        for item in services:
            if item.isAvailableInRunlevel(runlevelobj):
                status = item.status.strip().replace("\n",", ")[:32]
                if item in runlevelobj.activeservices:
                    lvi = QListViewItem(self.servicelistview,item.filename,i18n("Yes"),status)
                else:
                    lvi = QListViewItem(self.servicelistview,item.filename,i18n("No"),status)
                self.servicestolistitems[item] = lvi

    ########################################################################
    def __selectService(self,service):
        if service!=None:
            self.descriptiontextedit.setEnabled(True)
            self.descriptiontextedit.setText(service.description)

            if isroot:
                self.startatbootcheckbox.setEnabled(True)

            self.startatbootcheckbox.setChecked(self.currentrunlevel.isActiveAtBoot(service))
            self.statustext.setEnabled(True)
            self.statustext.setText(service.status)
        else:
            self.disableStuff()
            self.descriptiontextedit.setText("")
            self.descriptiontextedit.setEnabled(False)
            self.statustext.setText("")
            self.statustext.setEnabled(False)

    ########################################################################
    def slotRunLevelChanged(self,levelnum):
        if self.updatingGUI:
            return
        self.updatingGUI = True

        self.currentrunlevel = self.context.runlevels[levelnum]
        self.__fillListView(self.currentrunlevel)
        self.__selectFirstService()
        self.__checkServiceStatus()

        self.updatingGUI = False

    ########################################################################
    def slotListClicked(self,item):
        if self.updatingGUI:
            return
        self.updatingGUI = True

        for service in self.servicestolistitems.keys():
            if self.servicestolistitems[service] is item:
                self.selectedservice = service
                self.__selectService(self.selectedservice)
                break
        self.updatingGUI = False

    ########################################################################
    def slotBootChanged(self,state):
        if self.updatingGUI:
            return
        self.updatingGUI = True
        level = self.currentrunlevel
        level.setActiveAtBoot(self.selectedservice,state)
        if level.isActiveAtBoot(self.selectedservice):
            self.servicestolistitems[self.selectedservice].setText(1,i18n("Yes"))
        else:
            self.servicestolistitems[self.selectedservice].setText(1,i18n("No"))
        self.updatingGUI = False                         

    ########################################################################
    def slotCloseButton(self):
        self.close()

    ########################################################################
    def slotStartButton(self):
        self.__runInitScript(i18n("Starting %1").arg(self.selectedservice.filename), \
                             self.selectedservice,"start")

    ########################################################################
    def slotStopButton(self):
        self.__runInitScript(i18n("Stopping %1").arg(self.selectedservice.filename), \
                             self.selectedservice,"stop")

    ########################################################################
    def slotRestartButton(self):
        self.__runInitScript(i18n("Restarting %1").arg(self.selectedservice.filename), \
                             self.selectedservice,"restart")

    ########################################################################
    def slotZapButton(self):
        """This button lets the Gentoo user use the zap command, if process
        information has not properly been cleaned up by the init script.
        """
        if DISTRO == "Gentoo":
            self.__runInitScript(i18n("Zapping %1").arg(self.selectedservice.filename), \
                             self.selectedservice,"zap")


    ########################################################################
    def __startLoadDescriptions(self):
        if DISTRO=="Debian":
            cachepath = "/var/tmp"
            cachefile = "guidance-packagedescriptioncache"
            self.context.descriptioncache = DescriptionCache(cachefile,cachepath)
            self.context.descriptioncache.loadCache()
        self.__loadDescriptions()

    ########################################################################
    def __loadDescriptions(self):
        """
        Loads the description of all services showing a progressbar if it takes longer,
        tries to get the descriptions from cache first.
        """
        for service in self.context.services:
            if service.packagename is None:
                continue
            # Check if we want to fetch a description for the currently selected item
            # before we go on fetching other descriptions.
            if not self.selectedservice.packagename:
                self.selectedservice.fetchDescription()
                self.slotListClicked(self.servicelistview.currentItem())
                break
            if not service.packagename:
                service.fetchDescription()
                break
        else:
            self.slotListClicked(self.servicelistview.currentItem())
            if DISTRO=="Debian":
                self.context.descriptioncache.saveCache()
            return

        QTimer.singleShot(0,self.__loadDescriptions)

    ########################################################################
    def __runInitScript(self,title,service,command):        
        global DISTRO
        self.cr.setCaption(title)
        self.cr.setHeading(title)
        if command=="start":
            cmd = service.doStartCommand()
        elif command=="stop":
            cmd = service.doStopCommand()
        elif command=="restart":
            cmd = service.doRestartCommand()
        elif DISTRO == "Gentoo" and command=="zap":
            cmd = service.doZapCommand()

        self.cr.run(["/bin/bash","-c",cmd])

        # Does not seem to properly update ...
        self.__checkServiceStatus()

    ########################################################################
    def __checkServiceStatus(self):
        global kapp
        global progcount

        # Put up the progress dialog. (User pacifier).
        dialog = KProgressDialog(self,"statusprogress",
            i18n("Querying System Service Status"),
            i18n("Querying system service status"))
        dialog.setLabel(i18n("Querying system service status"))
        dialog.setAutoClose(True)
        dialog.showCancelButton(False)

        services = self.currentrunlevel.availableservices[:]
        services.sort(lambda x,y: cmp(x.filename,y.filename))
        dialog.progressBar().setTotalSteps(len(services))

        kapp.processEvents()
        self.updatingGUI = True

        for item in services:
            if item.gotStatus==False:
                item.fetchStatus()
                lvi = self.servicestolistitems[item]
                lvi.setText(2,item.status.strip().replace("\n",", ")[:32])
                if self.selectedservice is item:
                    self.statustext.setText(item.status)
            dialog.progressBar().advance(1)
            kapp.processEvents()
        dialog.setMinimumDuration(2000000000)
        dialog.hide()
        self.updatingGUI = False

    ########################################################################
    def __loadOptions(self):
        self.config.setGroup("General")
        size = self.config.readSizeEntry("Geometry")
        if size.isEmpty()==False:
            self.resize(size)
        size = self.config.readSizeEntry("CommandRunnerGeometry")
        if size.isEmpty()==False:
            self.cr.resize(size)

    #######################################################################
    def __saveOptions(self):
        global isroot
        if isroot:
            return
        self.config.setGroup("General")
        self.config.writeEntry("Geometry", self.size())
        self.config.writeEntry("CommandRunnerGeometry",self.cr.size())
        self.config.sync()

    #######################################################################
    # KControl virtual void methods
    def load(self):
        pass
    def save(self):
        pass
    def defaults(self):
        pass        
    def sysdefaults(self):
        pass

    def aboutData(self):
        # Return the TDEAboutData object which we created during initialisation.
        return self.aboutdata

    def buttons(self):
        # Only supply a Help button. Other choices are Default and Apply.
        return TDECModule.Help

############################################################################
class CommandRunner(KDialogBase):
    ########################################################################
    def __init__(self,parent,name):
        KDialogBase.__init__(self,parent,name,1,"",KDialogBase.Ok)
        self.output = ""
        self.running = False        

        self.resize(400,200)
        vbox = self.makeVBoxMainWidget()

        hbox = QHBox(vbox)
        hbox.setSpacing(self.spacingHint())

        tmplabel = QLabel(hbox)
        tmplabel.setPixmap(UserIcon("laserwarn"))
        hbox.setStretchFactor(tmplabel,0)

        self.headinglabel = QLabel(hbox)
        hbox.setStretchFactor(self.headinglabel,1)

        self.outputtextview = QTextView(vbox)
        self.outputtextview.setTextFormat(QTextView.PlainText)

        self.kid = QProcess()
        self.kid.setCommunication(QProcess.Stdout|QProcess.Stderr)
        self.connect(self.kid,SIGNAL("processExited()"),self.slotProcessExited)
        self.connect(self.kid,SIGNAL("readyReadStdout()"),self.slotReadyReadStdout)
        self.connect(self.kid,SIGNAL("readyReadStderr()"),self.slotReadyReadStderr)

    ########################################################################
    def run(self,argslist):
        self.kid.clearArguments()
        for arg in argslist:
            self.kid.addArgument(arg)
        self.output = ""
        self.outputtextview.setText(self.output)
        self.bootstraptimer = self.startTimer(0)
        self.running = True
        self.enableButtonOK(False)
        self.exec_loop()

    ########################################################################
    def timerEvent(self,timer):
        self.killTimer(self.bootstraptimer)

        # Create a slightly new environment where TERM is vt100
        new_env = QStringList()
        for key in os.environ:
            if key=="TERM":
                new_env.append("TERM=vt100")
            else:
                new_env.append(key + "=" + os.environ[key])

        self.kid.launch("",new_env)

    ########################################################################
    def setHeading(self,heading):
        self.headinglabel.setText(heading)

    ########################################################################
    def slotOKButton(self):
        self.accept()        

    ########################################################################
    def slotReadyReadStdout(self):
        # Remove the colors used by some programs.
        # FIXME: this probably isn't neccessary anymore.
        uncolor = lambda text: re.compile('\\x1b\[[0-9]+;01m').sub("", \
          re.compile('\\x1b\[0m').sub("", re.compile('\\033\[1;[0-9]+m').sub("", \
          re.compile('\\033\[0m').sub("", text))))
        self.output += uncolor(unicode(self.kid.readStdout()))
        self.outputtextview.setText(self.output)
        self.outputtextview.ensureVisible(0,self.outputtextview.contentsHeight())

    ########################################################################
    def slotReadyReadStderr(self):
        self.output += unicode(self.kid.readStderr())
        self.outputtextview.setText(self.output)
        self.outputtextview.ensureVisible(0,self.outputtextview.contentsHeight())

    ########################################################################
    def slotProcessExited(self):
        self.running = False        
        self.enableButtonOK(True)

############################################################################
# Factory function for KControl
def create_serviceconfig(parent,name):
    global kapp
    kapp = TDEApplication.kApplication()
    return SysVInitApp(parent, name)

############################################################################
def MakeAboutData():
    aboutdata = TDEAboutData("guidance",programname,version, \
        "Services Configuration Tool", TDEAboutData.License_GPL, \
        "Copyright (C) 2003-2007 Simon Edwards", \
        "Thanks go to  Phil Thompson, Jim Bublitz and David Boddie.")
    aboutdata.addAuthor("Simon Edwards","Developer","simon@simonzone.com","http://www.simonzone.com/software/")
    aboutdata.addAuthor("Sebastian Kügler","Developer","sebas@kde.nl","http://vizZzion.org");
    return aboutdata

if standalone:
    aboutdata = MakeAboutData()
    TDECmdLineArgs.init(sys.argv,aboutdata)

    kapp = TDEApplication()
    sysvapp = SysVInitApp()
    sysvapp.exec_loop()
