#!/usr/bin/env python3
# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Sigma agent upgrade tool.

Usage: %prog [-u URL] [-m MD5] load VERSION
       %prog activate VERSION
       %prog unload VERSION
       %prog xml FILENAME
       %prog [-t OSTYPE] create VERSION DIRNAME
       %prog cleanup
       %prog list
"""

import os
import hashlib
import sys
import glob
import stat
import time
import shutil
import subprocess
import urllib.request
import urllib.parse
import logging
import zipfile
import optparse
from io import StringIO

from sigma.util import avalog, xmlparse, ostypes, fslock
from sigma.util.config import Options, ES_PORT
from sigma.util.errors import usage
from sigma.protocol import xmlproto
from sigma.gather import gathering
from sigma.sched import remote, process

parser = optparse.OptionParser(usage=__doc__.strip())
parser.add_option("-u", "--url", action="store", dest="url",
                  help="URL for the upgrade zip file")
parser.add_option("-m", "--md5", action="store", dest="md5",
                  help="MD5 checksum of upgrade zip file")
parser.add_option("-t", "--type", action="store", dest="ostype",
                  help="Override operating system type")

options = Options()

logger = logging.getLogger("upgrade")

def main(args=None):
    avalog.setupLogging()
    opts, args = parser.parse_args(args)
    if not args:
        parser.print_help()
        return 0
    p = Processor(opts, args[0], args[1:])
    return p.run()

class Processor(object):

    def __init__(self, opts, cmd, args):
        self.opts = opts
        self.cmd = cmd
        self.args = args

    def run(self):
        method = getattr(self, "do_" + self.cmd, None)
        if method is None:
            return usage("unrecognized command %r", self.cmd)
        return method()

    def do_list(self):
        if self.args:
            return usage("'list' does not take arguments")
        vdict = getversions()
        active = getactive()
        keys = vdict.keys()
        keys = sorted(keys)
        for version in keys:
            if version == active:
                status = "A"
            else:
                status = "L"
            print("%-15s  %s  %s" % (version, status, vdict[version]))

    def do_load(self):
        # Check arguments
        if len(self.args) != 1:
            return usage("'load' requires exactly one VERSION argument")
        self.version = self.args[0]
        url = self.opts.url
        if url and url.startswith("/"):
            host, port = urllib.parse.splitport(options.central_server)
            # central-server may omit the port (e.g. "208.77.218.212"); fall back
            # to the default SSL port. The upgrade zip is served over plain HTTP at
            # the SSL port + 2.
            if port is None:
                port = ES_PORT
            port = int(port) + 2
            url = "http://%s:%d%s" % (host, port, url)
        self.url = url
        self.md5 = self.opts.md5
        if self.md5 is not None:
            self.md5 = self.md5.lower()

        # Check if the version is already downloaded
        vdict = getversions()
        if self.version in vdict:
            logger.info("Version %s already loaded", self.version)
            return 0

        # Create the versions directory if needed
        if not os.path.exists(options.agentversions):
            try:
                os.makedirs(options.agentversions)
            except os.error as err:
                logger.error("Can't create %s: %s",
                             options.agentversions, err)
                return 1

        # Check the upgrade zip file
        zfn = os.path.join(options.agentversions, self.version + ".zip")
        have = os.path.exists(zfn)
        if have and self.md5:
            if md5file(zfn) == self.md5:
                logger.info("Already have the correct %s", zfn)
            else:
                if self.url:
                    logger.info("Deleting previous incorrect %s", zfn)
                    # XXX error checking
                    os.remove(zfn)
                have = False

        if not have:
            # Must specify a URL in this case
            if self.url is None:
                return usage("'load' command requires '-u URL' option")

            # Download the upgrade zip file
            logger.debug("Downloading %s to %s", self.url, zfn)
            try:
                MyURLopener().retrieve(self.url, zfn)
            except Exception as err:
                logger.error("Can't retrieve %s: %s", self.url, err)
                return 1
            logger.info("Downloaded %s to %s", self.url, zfn)

            # Verify MD5 checksum of upgrade zip file
            if self.md5:
                actual = md5file(zfn)
                if actual != self.md5:
                    logger.error("Upgrade zip file MD5 checksum mismatch: "
                                 "expected %s, actual %s (size %s)",
                                 self.md5, actual, os.path.getsize(zfn))
                    return 1

        # Open the zip file and extract the MANIFEST.txt file; load the files
        try:
            self.zipfile = zipfile.ZipFile(zfn)
        except zipfile.BadZipfile as err:
            logger.error("Bad zip file %s: %s", zfn, err)
            return 1
        try:
            # ZipFile.read() returns bytes on Python 3; MANIFEST.txt is the
            # text manifest written by do_create (md5/filename columns), so decode
            # it before parsing and before writing it back out as a text file.
            manifest = self.zipfile.read("MANIFEST.txt").decode("utf-8")
            sts = self.loadfiles(manifest)
        finally:
            # close the zipfile
            self.zipfile.close()
        if sts:
            return sts

        # Save the MANIFEST.txt file marking this as an upgrade
        fn = os.path.join(options.agentversions, self.version, "MANIFEST.txt")
        try:
            f = open(fn, "w")
        except IOError as err:
            logger.error("Can't write %s: %s", fn, err)
            return 1
        try:
            f.write(manifest)
        finally:
            f.close()

        # Create the version.txt file marking this as a good version
        fn = os.path.join(options.agentversions, self.version, "version.txt")
        try:
            f = open(fn, "w")
        except IOError as err:
            logger.error("Can't write %s: %s", fn, err)
            return 1
        try:
            f.write(self.version + "\n")
        finally:
            f.close()

        # Successful exit
        return 0

    def loadfiles(self, manifest):
        lines_iter = iter(manifest.splitlines())
        headers = next(lines_iter).split()
        nfields = len(headers)
        try:
            imd5 = headers.index("md5")
            ifilename = headers.index("filename")
            assert ifilename == nfields - 1
        except (ValueError, AssertionError) as err:
            logger.error("Invalid manifest file header: %r", headers)
            return 1
        lineno = 1
        errors = 0
        files = 0
        for line in lines_iter:
            lineno += 1
            fields = line.split(None, ifilename)
            if len(fields) != nfields:
                logger.warning("Ignoring invalid manifest line (%d): %r",
                               lineno, line)
                continue
            md5sum = fields[imd5].lower()
            filename = fields[ifilename]
            n = self.loadfile(md5sum, filename)
            errors += n
            files += (not n)
        if errors:
            logger.warning("Loaded %d files, %d errors", files, errors)
            return 1
        logger.info("Loaded %d files", files)
        return 0

    def loadfile(self, md5sum, filename):
        oldname = os.path.join(options.agentactive, filename)
        newname = os.path.join(options.agentversions, self.version, filename)
        dirname = os.path.dirname(newname)
        if not os.path.exists(dirname):
            try:
                os.makedirs(dirname)
            except os.error as err:
                logger.error("Can't create %s: %s", dirname, err)
                return 1
        if os.path.exists(newname):
            try:
                os.remove(newname)
            except os.error as err:
                logger.warning("Can't remove existing %s: %s", newname, err)
        copied = False
        if os.path.exists(oldname):
            oldmd5 = md5file(oldname)
            if oldmd5 == md5sum:
                try:
                    shutil.copy2(oldname, newname)
                    assert md5file(newname) == md5sum # XXX
                    logger.debug("Copied %s to %s", oldname, newname)
                    copied = True
                except (IOError, os.error) as err:
                    logger.warning("Can't copy %s to %s: %s",
                                oldname, newname, err)
        if os.sep != "/":
            # Little-known fact: zip files always uses forward slashes
            filename = filename.replace(os.sep, "/")
        if not copied:
            try:
                data = self.zipfile.read(filename)
            except KeyError:
                logger.error("Can't find %s in zipfile", filename)
                return 1
            if md5data(data) != md5sum:
                logger.error("Zipfile contents for %s has wrong MD5", filename)
                return 1
            f = open(newname, "wb")
            try:
                f.write(data)
            finally:
                f.close()
            logger.debug("Created %s from zipfile", newname)
        if os.name == "posix":
            # Set Unix x permissions
            zi = self.zipfile.getinfo(filename)
            mode = zi.external_attr >> 16
            if mode & 0o100:
                st = os.stat(newname)
                mode = st.st_mode & 0o7777
                mode = mode | ((mode & 0o444) >> 2)
                os.chmod(newname, mode)
                logger.debug("Chmod 0%o %s", mode, newname)
        return 0

    def do_unload(self):
        if len(self.args) != 1:
            return usage("'unload' requires exactly one VERSION argument")
        version = self.args[0]
        if version == getactive():
            logger.error("Can't unload the active version (%s)", version)
            return 1
        vdict = getversions()
        dn = vdict.get(version)
        if not dn:
            logger.info("Version %s is not loaded", version)
        elif not os.path.exists(os.path.join(dn, "MANIFEST.txt")):
            logger.warning("Version %s has no MANIFEST.txt; not unloading",
                        version)
        else:
            logger.debug("Unloading version %s, directory %s", version, dn)
            rmtree(dn)
            logger.info("Unloaded version %s, directory %s", version, dn)
        zfn = os.path.join(options.agentversions, version + ".zip")
        if os.path.exists(zfn):
            try:
                os.remove(zfn)
            except os.error as err:
                logger.warning("Can't remove %s: %s", zfn, err)
            else:
                logger.info("Removed %s", zfn)
        return 0

    def do_cleanup(self):
        """Remove old versions.

        This removes:

        - all but the last three inactive versions
        - files and directories older than a day except complete versions

        Run this nightly.

        """
        if self.args:
            return usage("'cleanup' takes no arguments")
        if not os.path.exists(options.agentversions):
            logger.debug("Can't find %s", options.agentversions)
            return # Nothing to do
        now = time.time()
        then = now - 24*3600 # One day ago
        active = getactive()
        vdict = getversions()
        vdirs = [os.path.normcase(os.path.basename(dn))
                 for dn in vdict.values()]
        # Delete non-versions older than a day
        try:
            names = os.listdir(options.agentversions)
        except os.error as err:
            logger.error("Can't list %s: %s", options.agentversions, err)
            return
        for name in names:
            name = os.path.normcase(name)
            if name in vdirs:
                logger.debug("Skipping version %s", name)
            else:
                fn = os.path.join(options.agentversions, name)
                try:
                    mtime = os.path.getmtime(fn)
                except os.error as err:
                    logger.warning("Can't stat %s: %s", fn, err)
                    continue
                if mtime >= then:
                    logger.debug("Keeping young %s", name)
                    continue
                if os.path.isdir(fn) and not os.path.islink(fn):
                    logger.info("Removing directory %s", fn)
                    rmtree(fn)
                else:
                    try:
                        os.remove(fn)
                    except os.error as err:
                        logger.warning("Can't remove %s: %s", fn, err)
                    else:
                        logger.info("Removed %s", fn)
        # Make a table of inactive non-installed versions ordered by
        # when their versions.txt file was written
        table = []
        for version, dn in vdict.items():
            if version == active:
                logger.debug("Keeping active version %s", version)
                continue
            mfn = os.path.join(dn, "MANIFEST.txt")
            if not os.path.exists(mfn):
                logger.debug("Keeping installed version %s", version)
                continue
            vfn = os.path.join(dn, "version.txt")
            try:
                mtime = os.path.getmtime(vfn)
            except os.error as err:
                logger.warning("Can't stat %s: %s", vfn, err)
                continue
            logger.debug("Considering version %s", version)
            table.append((mtime, version, dn))
        table.sort()
        # Delete all except the last three versions in the table
        for mtime, version, dn in table[:-3]:
            logger.debug("Unloading version %s, directory %s", version, dn)
            rmtree(dn)
        for mtime, version, db in table[-3:]:
            logger.debug("Not unloading version %s", version)

    def do_activate(self):

        # Various argument sanity checks
        if len(self.args) != 1:
            return usage("'activate' requires exactly one VERSION argument")
        activedn = options.agentactive
        newversion = self.args[0]
        oldversion = getactive()
        if newversion == oldversion:
            logger.info("Version %s is already active", newversion)
            return 0
        vdict = getversions()
        newdn = vdict.get(newversion)
        if not newdn:
            logger.error("Version %s not found", newversion)
            return 1
        olddn = vdict.get(oldversion)
        if not olddn or olddn == options.agenthome:
            # We wouldn't be able to roll back, so refuse to activate
            logger.error("Can't switch from outdated filesystem lay-out")
            return 1

        logger.info("Activating version %s at %s", newversion, newdn)

        # Shut down the old scheduler
        mustrestart = iswinsvc = False
        svcname = options.service_name
        sched = remote.Scheduler(options.scheduler)
        try:
            sched.ping()
        except Exception as err:
            logger.info("Scheduler not running")
        else:
            mustrestart = True
            if os.name == "nt": # Windows
                import win32serviceutil, win32service
                try:
                    status = win32serviceutil.QueryServiceStatus(svcname)[1]
                except win32service.error as err:
                    pass
                else:
                    iswinsvc = (status == win32service.SERVICE_RUNNING)
            if iswinsvc:
                logger.info("Stopping %s service", svcname)
                try:
                    win32serviceutil.StopService(svcname)
                except win32service.error as err:
                    logger.error("%s service not stopped: %s", svcname, err)
            else:
                # XXX Error checking
                logger.info("Stopping scheduler")
                sched.shutdown()

        # Unload the poliwall kernel module, if necessary
        run_pw_unload()

        # Do the switch
        sts = switchversions(oldversion, newversion, vdict)
        if not sts: # I.e., if the switch was successful
            # Now test communication with the server.  We do this by
            # doing a hostname gather and then invoking the
            # transfer-mgr.py with an argument of "hostname.full"
            # (which limits the uploaded data to the hostname gather).
            # If this fails for any reason, we restore the previously
            # active version.  (If it was a temporary glitch, the
            # server will tell us to switch again later and it will
            # succeed.)
            logger.info("Running test gather...")
            try:
                g = gathering.Gatherer(options)
                g.full_gather("hostname")
            except Exception as err:
                logger.error("Test gather failed: %s: %s",
                             err.__class__.__name__, err)
                sts = 1
            else:
                logger.info("Running connection test...")
                transfer_mgr = os.path.join(activedn, "bin", "transfer-mgr.py")
                args = [sys.executable, transfer_mgr, "hostname.full"]
                proc = process.createProcess(args)
                proc.wait()
                sts = proc.getstatus()
            if sts:
                logger.error("Gather+connection test failed, rolling back")
                sts = switchversions(newversion, oldversion, getversions())
                if sts:
                    logger.critical("Rollback failed!  Agent stopped")
                    return sts
                logger.info("Reactivated old version %s at %s",
                            oldversion, olddn)
            else:
                logger.info("Connection test passed!")

        if mustrestart:
            # Start the new scheduler
            # XXX This assumes the new version has the same fs lay-out
            # XXX This is screwed when the service is renamed
            if iswinsvc:
                logger.info("Starting %s service", svcname)
                import win32serviceutil, win32service
                try:
                    win32serviceutil.StartService(svcname)
                    avsystray = os.path.join(activedn, "bin", "avsystray.exe")
                    avsystray_args = [sys.executable, avsystray]
                    try:
                        proc = process.createProcess(avsystray_args)
                    except process.error as err:
                        logger.error("Can't start AvSysTray.exe: %s", err)
                    else:
                        logger.info("Started AvSysTray.exe, pid = %d", proc.getpid())
                except win32service.error as err:
                    logger.error("%s service not started: %s", svcname, err)
            else:
                logger.info("Starting scheduler")
                avbin = os.path.join(activedn, "bin")
                avlib = os.path.join(activedn, "lib")
                fuelpath = os.path.join(avlib, "fuel")
                os.environ["AVACTIVE"] = activedn
                os.environ["AVBIN"] = avbin
                os.environ["AVLIB"] = avlib
                os.environ["FUELPATH"] = fuelpath
                scheduler = os.path.join(avbin, "scheduler.py")
                args = [sys.executable, scheduler, "-d"]
                try:
                    proc = process.createProcess(args)
                except process.error as err:
                    logger.error("Can't start scheduler: %s", err)
                else:
                    logger.info("Started scheduler, pid = %d", proc.getpid())
        else:
            logger.info("Not starting scheduler")

        return sts

    def do_xml(self):
        if len(self.args) != 1:
            return usage("'xml' command requires "
                         "exactly one FILENAME argument")
        filename = self.args[0]
        p = xmlparse.GenericParser([xmlproto.Command])
        try:
            f = open(filename)
        except IOError as err:
            logger.error("Can't open %s: %s", filename, err)
            return 1
        sts = 0
        try:
            command = p.parseStream(f)
        finally:
            f.close()
        lockdir = options.datapath("upgrade.lock")
        logger.info("Locking %s", lockdir)
        lock = fslock.FSLock(lockdir)
        if not lock.lock(30*60):
            logger.error("Giving up on locking %s", lockdir)
            return 1
        logger.info("Acquired lock %s", lockdir)
        for upg in command.upgradeList:
            assert upg.action in ("load", "activate", "unload"), upg.action
            assert upg.version is not None
            p = Processor(upg, upg.action, [str(upg.version)])
            if p.run():
                sts = 1
        logger.info("Releasing lock %s", lockdir)
        lock.unlock()
        g = gathering.Gatherer(options)
        g.full_gather("upgrade")
        return sts

    def do_create(self):
        if len(self.args) != 2:
            return usage("'create' command requires "
                         "VERSION and DIRNAME arguments")
        version = self.args[0]
        dirname = self.args[1]
        if self.opts.ostype:
            # We intentionally do not validate this value!
            ost = self.opts.ostype
        else:
            ost = ostypes.guess_ostype()
        zfn = "%s.%s.zip" % (version, ost)
        mf = StringIO()
        zf = zipfile.ZipFile(zfn, "w", zipfile.ZIP_DEFLATED)
        mf.write("%-32s %s\n" % ("md5", "filename"))
        count = 0
        self.walk_errors = 0
        # Skip our own output zip: when create is run from inside the payload
        # dir (package-windows.ps1 does `upgrade.py create <ver> .` from the
        # staging dir), the freshly-opened zfn sits in the walked tree and would
        # be self-included as a 0-byte nested <ver>.<ost>.zip. Exclude it.
        special_files = ("version.txt", "MANIFEST.txt", "UPGRADE.txt",
                         os.path.basename(zfn))
        cwd = os.getcwd()
        try:
            os.chdir(dirname)
            prefix = os.path.join(os.curdir, "")
            for dn, dirs, files in self.walk(os.curdir):
                if dn == os.curdir:
                    dn = ""
                elif dn.startswith(prefix):
                    dn = dn[len(prefix):]
                dirs.sort()
                files.sort()
                for file in files:
                    filename = os.path.join(dn, file)
                    if filename not in special_files:
                        sum = md5file(filename)
                        if sum:
                            mf.write("%s %s\n" % (sum, filename))
                            zf.write(filename)
                            count += 1
        finally:
            os.chdir(cwd)
        zf.writestr("MANIFEST.txt", mf.getvalue())
        zf.writestr("UPGRADE.txt", "OS=%s\nVersion=%s\n" % (ost, version))
        zf.close()
        size = os.path.getsize(zfn)
        print("Counted %d files" % count)
        print("Wrote zipfile %s (%.3g Mbytes)" % (zfn, size/1e6))
        md5sum = md5file(zfn)
        if self.opts.url:
            url = urllib.parse.urljoin(self.opts.url, zfn)
            print("SQL template:")
            print("INSERT INTO upgrade (version, ostype, url, md5)")
            print("VALUES ('%s'," % version)
            print("        '%s'," % ost)
            print("        '%s'," % url)
            print("        '%s');" % md5sum)
        return bool(self.walk_errors)

    def walk(self, top):
        # A variation on the os.walk() generator that's more uptight
        # about errors.  This also follows symlinks (since I don't
        # know how to put symlinks in zip files, and we occasionally
        # have a symlink in the distribution).
        try:
            names = os.listdir(top)
        except os.error as err:
            self.walk_errors += 1
            print("Can't listdir %s: %s" % (top, err))
            return
        dirs, nondirs = [], []
        for name in names:
            path = os.path.join(top, name)
            try:
                st = os.stat(path)
            except os.error as err:
                self.walk_errors += 1
                print("Can't stat %s: %s" % (path, err))
                continue
            if stat.S_ISDIR(st.st_mode):
                dirs.append(name)
            else:
                nondirs.append(name)
        yield top, dirs, nondirs
        for name in dirs:
            path = os.path.join(top, name)
            for x in self.walk(path):
                yield x

class MyURLopener(urllib.request.FancyURLopener):

    """Override the default HTTP error handler to raise IOError."""

    http_error_default = urllib.request.URLopener.http_error_default

def md5file(filename):
    # Return the MD5 checksum (in hex, lowercased) of a file in binary mode
    BUFSIZE = 8*1024
    summer = hashlib.md5()
    try:
        f = open(filename, "rb")
    except IOError as err:
        logger.error("Can't open %s: %s", filename, err)
        return None
    try:
        while 1:
            data = f.read(BUFSIZE)
            if not data:
                break
            summer.update(data)
    finally:
        f.close()
    return summer.hexdigest().lower()

def md5data(data):
    # Return the MD5 checksum (in hex, lowercased) of a string
    return hashlib.md5(data).hexdigest().lower()

def switchversions(oldversion, newversion, vdict):
    activedn = options.agentactive
    newdn = vdict[newversion]
    if os.name != "nt":
        # Unix -- can use atomic rename.
        # NB: this used to test hasattr(os, "symlink") to detect Unix, which
        # worked under Python 2 (os.symlink was absent on Windows) but is
        # ALWAYS true under Python 3.  On Windows that wrongly took this
        # branch, where os.rename() cannot overwrite the existing 'active'
        # junction and raised -- so Windows agents downloaded a new version
        # but could never activate it (loaded != active forever).  Route
        # Windows to the rename-aside branch below, which is what it did
        # under Python 2.
        tn = activedn + ".new"
        try:
            if os.path.exists(tn):
                os.remove(tn)
            os.symlink(newdn, tn)
            os.rename(tn, activedn)
        except os.error as err:
            logger.error("Version switch failed: %s", err)
            return 1
    else:
        # Windows -- 'active' is a directory junction created by the installer
        # (cmd /c rmdir active & mklink /J active <dir>).  Repoint it the SAME
        # way rather than renaming it: 'active' contains the running python.exe,
        # and Windows refuses to move a directory that holds an in-use image, so
        # the old rename-aside dance failed here.  Dropping a junction only
        # unlinks the alias -- the target, and the running exe under it, are
        # untouched -- then mklink /J re-aliases it to newdn.  This mirrors the
        # MSI's own junction handling, which repoints 'active' while the service
        # is live.
        def _relink(target):
            # rmdir removes the junction reparse point (NOT its target); mklink
            # recreates it.  Both are cmd built-ins, so they must run through
            # cmd.exe.  Pass each as a separate argument list (NOT one shell
            # string): Windows argv-quoting turns embedded "..." into \"...\",
            # which cmd cannot parse, and the compound 'rmdir & mklink' string
            # fails with "The filename, directory name, or volume label syntax
            # is incorrect."  rmdir is best-effort (absent 'active' is fine).
            subprocess.call(["cmd", "/c", "rmdir", activedn])
            subprocess.check_call(["cmd", "/c", "mklink", "/J", activedn, target])
        try:
            _relink(newdn)
        except (OSError, subprocess.CalledProcessError) as err:
            logger.error("Version switch failed: %s", err)
            olddn = vdict.get(oldversion)
            if olddn:
                # Don't leave 'active' dangling if the relink half-failed.
                try:
                    _relink(olddn)
                    logger.info("(Restored previous 'active' junction.)")
                except Exception as err2:
                    logger.error("Failed to restore 'active': %s", err2)
            return 1
    logger.info("Activated version %s at %s", newversion, newdn)
    return 0

# XXX The following two are similar to code in gather_upgrade.py...

def getactive():
    # Return the version number of the active version; None if none found
    avfn = os.path.join(options.agentactive, "version.txt")
    try:
        f = open(avfn)
    except IOError as err:
        logger.warning("Can't read active version file %s: %s", avfn, err)
        return None
    try:
        av = f.readline().strip()
    finally:
        f.close()
    return av or None

def getversions():
    # Return a dict mapping version numbers to directories
    pattern = os.path.join(options.agentversions, "*", "version.txt")
    d = {}
    for vfn in glob.glob(pattern):
        try:
            f = open(vfn)
        except IOError as err:
            logger.warning("Can't read version file %s: %s", vfn, err)
            continue
        try:
            v = f.readline().strip()
        finally:
            f.close()
        if v:
            vdn = os.path.dirname(vfn)
            if v in d:
                logger.error("Duplicate version %s found at %s and %s",
                             v, d[v], vdn)
            d[v] = vdn
    av = getactive()
    if av and av not in d:
        d[av] = options.agentactive
    return d

def run_pw_unload():
    pw_unload = "pw_unload"
    if os.name == "nt":
        pw_unload += ".bat"
    pw_unload = os.path.join(options.agentlib, "modules", pw_unload)
    if not os.path.exists(pw_unload):
        logger.debug("can't find %s", pw_unload)
        return
    args = [pw_unload]
    if os.name == "nt":
        args = ["cmd", "/c"] + args
    env = os.environ.copy()
    env["AVBIN"] = options.agentbin
    env["AVLIB"] = options.agentlib
    env["AVDATA"] = options.agentdata
    logger.debug("args = %s", args)
    proc = process.createProcess(args, env=env)
    proc.wait()
    return

def rmtree(dn):
    try:
        names = os.listdir(dn)
    except os.error:
        return
    for name in names:
        fullname = os.path.join(dn, name)
        if os.path.isdir(fullname) and not os.path.islink(fullname):
            rmtree(fullname)
        else:
            try:
                os.remove(fullname)
            except os.error:
                if os.name == "nt":
                    os.chmod(fullname, 0o777)
                    try:
                        os.remove(fullname)
                    except os.error:
                        pass
    try:
        os.rmdir(dn)
    except os.error:
        pass

if __name__ == "__main__":
    sys.exit(main())
