#!/usr/bin/env python3

# Copyright (c) 2009 Giampaolo Rodola'. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Cross-platform lib for process and system monitoring in Python."""

from __future__ import print_function

import contextlib
import io
import os
import platform
import re
import shutil
import struct
import subprocess
import sys
import tempfile
import warnings


with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    try:
        import setuptools
        from setuptools import Extension
        from setuptools import setup
    except ImportError:
        setuptools = None
        from distutils.core import Extension
        from distutils.core import setup

HERE = os.path.abspath(os.path.dirname(__file__))

# ...so we can import _common.py and _compat.py
sys.path.insert(0, os.path.join(HERE, "psutil"))

from _common import AIX  # NOQA
from _common import BSD  # NOQA
from _common import FREEBSD  # NOQA
from _common import LINUX  # NOQA
from _common import MACOS  # NOQA
from _common import NETBSD  # NOQA
from _common import OPENBSD  # NOQA
from _common import POSIX  # NOQA
from _common import SUNOS  # NOQA
from _common import WINDOWS  # NOQA
from _common import hilite  # NOQA
from _compat import PY3  # NOQA
from _compat import which  # NOQA


PYPY = '__pypy__' in sys.builtin_module_names
macros = []
if POSIX:
    macros.append(("PSUTIL_POSIX", 1))
if BSD:
    macros.append(("PSUTIL_BSD", 1))

# Needed to determine _Py_PARSE_PID in case it's missing (Python 2, PyPy).
# Taken from Lib/test/test_fcntl.py.
# XXX: not bullet proof as the (long long) case is missing.
if struct.calcsize('l') <= 8:
    macros.append(('PSUTIL_SIZEOF_PID_T', '4'))  # int
else:
    macros.append(('PSUTIL_SIZEOF_PID_T', '8'))  # long


sources = ['psutil/_psutil_common.c']
if POSIX:
    sources.append('psutil/_psutil_posix.c')


extras_require = {"test": [
    "enum34; python_version <= '3.4'",
    "ipaddress; python_version < '3.0'",
    "mock; python_version < '3.0'",
]}
if not PYPY:
    extras_require['test'].extend([
        "pywin32; sys.platform == 'win32'",
        "wmi; sys.platform == 'win32'"])


def get_version():
    INIT = os.path.join(HERE, 'psutil/__init__.py')
    with open(INIT, 'r') as f:
        for line in f:
            if line.startswith('__version__'):
                ret = eval(line.strip().split(' = ')[1])
                assert ret.count('.') == 2, ret
                for num in ret.split('.'):
                    assert num.isdigit(), ret
                return ret
        raise ValueError("couldn't find version string")


VERSION = get_version()
macros.append(('PSUTIL_VERSION', int(VERSION.replace('.', ''))))


def get_description():
    script = os.path.join(HERE, "scripts", "internal", "convert_readme.py")
    readme = os.path.join(HERE, 'README.rst')
    p = subprocess.Popen([sys.executable, script, readme],
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        raise RuntimeError(stderr)
    data = stdout.decode('utf8')
    if WINDOWS:
        data = data.replace('\r\n', '\n')
    return data


@contextlib.contextmanager
def silenced_output(stream_name):
    class DummyFile(io.BytesIO):
        # see: https://github.com/giampaolo/psutil/issues/678
        errors = "ignore"

        def write(self, s):
            pass

    orig = getattr(sys, stream_name)
    try:
        setattr(sys, stream_name, DummyFile())
        yield
    finally:
        setattr(sys, stream_name, orig)


def missdeps(msg):
    s = hilite("C compiler or Python headers are not installed ", color="red")
    s += hilite("on this system. Try to run:\n", color="red")
    s += hilite(msg, color="red", bold=True)
    print(s, file=sys.stderr)


if WINDOWS:
    def get_winver():
        maj, min = sys.getwindowsversion()[0:2]
        return '0x0%s' % ((maj * 100) + min)

    if sys.getwindowsversion()[0] < 6:
        msg = "this Windows version is too old (< Windows Vista); "
        msg += "psutil 3.4.2 is the latest version which supports Windows "
        msg += "2000, XP and 2003 server"
        raise RuntimeError(msg)

    macros.append(("PSUTIL_WINDOWS", 1))
    macros.extend([
        # be nice to mingw, see:
        # http://www.mingw.org/wiki/Use_more_recent_defined_functions
        ('_WIN32_WINNT', get_winver()),
        ('_AVAIL_WINVER_', get_winver()),
        ('_CRT_SECURE_NO_WARNINGS', None),
        # see: https://github.com/giampaolo/psutil/issues/348
        ('PSAPI_VERSION', 1),
    ])

    ext = Extension(
        'psutil._psutil_windows',
        sources=sources + [
            'psutil/_psutil_windows.c',
            'psutil/arch/windows/process_utils.c',
            'psutil/arch/windows/process_info.c',
            'psutil/arch/windows/process_handles.c',
            'psutil/arch/windows/disk.c',
            'psutil/arch/windows/net.c',
            'psutil/arch/windows/cpu.c',
            'psutil/arch/windows/security.c',
            'psutil/arch/windows/services.c',
            'psutil/arch/windows/socks.c',
            'psutil/arch/windows/wmi.c',
        ],
        define_macros=macros,
        libraries=[
            "psapi", "kernel32", "advapi32", "shell32", "netapi32",
            "ws2_32", "PowrProf", "pdh",
        ],
        # extra_compile_args=["/W 4"],
        # extra_link_args=["/DEBUG"]
    )

elif MACOS:
    macros.append(("PSUTIL_OSX", 1))
    ext = Extension(
        'psutil._psutil_osx',
        sources=sources + [
            'psutil/_psutil_osx.c',
            'psutil/arch/osx/process_info.c',
            'psutil/arch/osx/cpu.c',
        ],
        define_macros=macros,
        extra_link_args=[
            '-framework', 'CoreFoundation', '-framework', 'IOKit'
        ])

elif FREEBSD:
    macros.append(("PSUTIL_FREEBSD", 1))
    ext = Extension(
        'psutil._psutil_bsd',
        sources=sources + [
            'psutil/_psutil_bsd.c',
            'psutil/arch/freebsd/cpu.c',
            'psutil/arch/freebsd/mem.c',
            'psutil/arch/freebsd/disk.c',
            'psutil/arch/freebsd/sensors.c',
            'psutil/arch/freebsd/proc.c',
            'psutil/arch/freebsd/sys_socks.c',
            'psutil/arch/freebsd/proc_socks.c',
        ],
        define_macros=macros,
        libraries=["devstat"])

elif OPENBSD:
    macros.append(("PSUTIL_OPENBSD", 1))
    ext = Extension(
        'psutil._psutil_bsd',
        sources=sources + [
            'psutil/_psutil_bsd.c',
            'psutil/arch/openbsd/cpu.c',
            'psutil/arch/openbsd/disk.c',
            'psutil/arch/openbsd/mem.c',
            'psutil/arch/openbsd/proc.c',
        ],
        define_macros=macros,
        libraries=["kvm"])

elif NETBSD:
    macros.append(("PSUTIL_NETBSD", 1))
    ext = Extension(
        'psutil._psutil_bsd',
        sources=sources + [
            'psutil/_psutil_bsd.c',
            'psutil/arch/netbsd/specific.c',
            'psutil/arch/netbsd/socks.c',
        ],
        define_macros=macros,
        libraries=["kvm"])

elif LINUX:
    def get_ethtool_macro():
        # see: https://github.com/giampaolo/psutil/issues/659
        from distutils.errors import CompileError
        from distutils.unixccompiler import UnixCCompiler

        with tempfile.NamedTemporaryFile(
                suffix='.c', delete=False, mode="wt") as f:
            f.write("#include <linux/ethtool.h>")

        output_dir = tempfile.mkdtemp()
        try:
            compiler = UnixCCompiler()
            # https://github.com/giampaolo/psutil/pull/1568
            if os.getenv('CC'):
                compiler.set_executable('compiler_so', os.getenv('CC'))
            with silenced_output('stderr'):
                with silenced_output('stdout'):
                    compiler.compile([f.name], output_dir=output_dir)
        except CompileError:
            return ("PSUTIL_ETHTOOL_MISSING_TYPES", 1)
        else:
            return None
        finally:
            os.remove(f.name)
            shutil.rmtree(output_dir)

    macros.append(("PSUTIL_LINUX", 1))
    ETHTOOL_MACRO = get_ethtool_macro()
    if ETHTOOL_MACRO is not None:
        macros.append(ETHTOOL_MACRO)
    ext = Extension(
        'psutil._psutil_linux',
        sources=sources + ['psutil/_psutil_linux.c'],
        define_macros=macros)

elif SUNOS:
    macros.append(("PSUTIL_SUNOS", 1))
    ext = Extension(
        'psutil._psutil_sunos',
        sources=sources + [
            'psutil/_psutil_sunos.c',
            'psutil/arch/solaris/v10/ifaddrs.c',
            'psutil/arch/solaris/environ.c'
        ],
        define_macros=macros,
        libraries=['kstat', 'nsl', 'socket'])

elif AIX:
    macros.append(("PSUTIL_AIX", 1))
    ext = Extension(
        'psutil._psutil_aix',
        sources=sources + [
            'psutil/_psutil_aix.c',
            'psutil/arch/aix/net_connections.c',
            'psutil/arch/aix/common.c',
            'psutil/arch/aix/ifaddrs.c'],
        libraries=['perfstat'],
        define_macros=macros)

else:
    sys.exit('platform %s is not supported' % sys.platform)


if POSIX:
    posix_extension = Extension(
        'psutil._psutil_posix',
        define_macros=macros,
        sources=sources)
    if SUNOS:
        def get_sunos_update():
            # See https://serverfault.com/q/524883
            # for an explanation of Solaris /etc/release
            with open('/etc/release') as f:
                update = re.search(r'(?<=s10s_u)[0-9]{1,2}', f.readline())
                if update is None:
                    return 0
                else:
                    return int(update.group(0))

        posix_extension.libraries.append('socket')
        if platform.release() == '5.10':
            # Detect Solaris 5.10, update >= 4, see:
            # https://github.com/giampaolo/psutil/pull/1638
            if get_sunos_update() >= 4:
                # MIB compliancy starts with SunOS 5.10 Update 4:
                posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1))
            posix_extension.sources.append('psutil/arch/solaris/v10/ifaddrs.c')
            posix_extension.define_macros.append(('PSUTIL_SUNOS10', 1))
        else:
            # Other releases are by default considered to be new mib compliant.
            posix_extension.define_macros.append(('NEW_MIB_COMPLIANT', 1))
    elif AIX:
        posix_extension.sources.append('psutil/arch/aix/ifaddrs.c')

    extensions = [ext, posix_extension]
else:
    extensions = [ext]


def main():
    kwargs = dict(
        name='psutil',
        version=VERSION,
        description=__doc__ .replace('\n', ' ').strip() if __doc__ else '',
        long_description=get_description(),
        long_description_content_type='text/x-rst',
        keywords=[
            'ps', 'top', 'kill', 'free', 'lsof', 'netstat', 'nice', 'tty',
            'ionice', 'uptime', 'taskmgr', 'process', 'df', 'iotop', 'iostat',
            'ifconfig', 'taskset', 'who', 'pidof', 'pmap', 'smem', 'pstree',
            'monitoring', 'ulimit', 'prlimit', 'smem', 'performance',
            'metrics', 'agent', 'observability',
        ],
        author='Giampaolo Rodola',
        author_email='g.rodola@gmail.com',
        url='https://github.com/giampaolo/psutil',
        platforms='Platform Independent',
        license='BSD',
        packages=['psutil', 'psutil.tests'],
        ext_modules=extensions,
        classifiers=[
            'Development Status :: 5 - Production/Stable',
            'Environment :: Console',
            'Environment :: Win32 (MS Windows)',
            'Intended Audience :: Developers',
            'Intended Audience :: Information Technology',
            'Intended Audience :: System Administrators',
            'License :: OSI Approved :: BSD License',
            'Operating System :: MacOS :: MacOS X',
            'Operating System :: Microsoft :: Windows :: Windows 10',
            'Operating System :: Microsoft :: Windows :: Windows 7',
            'Operating System :: Microsoft :: Windows :: Windows 8',
            'Operating System :: Microsoft :: Windows :: Windows 8.1',
            'Operating System :: Microsoft :: Windows :: Windows Server 2003',
            'Operating System :: Microsoft :: Windows :: Windows Server 2008',
            'Operating System :: Microsoft :: Windows :: Windows Vista',
            'Operating System :: Microsoft',
            'Operating System :: OS Independent',
            'Operating System :: POSIX :: AIX',
            'Operating System :: POSIX :: BSD :: FreeBSD',
            'Operating System :: POSIX :: BSD :: NetBSD',
            'Operating System :: POSIX :: BSD :: OpenBSD',
            'Operating System :: POSIX :: BSD',
            'Operating System :: POSIX :: Linux',
            'Operating System :: POSIX :: SunOS/Solaris',
            'Operating System :: POSIX',
            'Programming Language :: C',
            'Programming Language :: Python :: 2',
            'Programming Language :: Python :: 2.7',
            'Programming Language :: Python :: 3',
            'Programming Language :: Python :: Implementation :: CPython',
            'Programming Language :: Python :: Implementation :: PyPy',
            'Programming Language :: Python',
            'Topic :: Software Development :: Libraries :: Python Modules',
            'Topic :: Software Development :: Libraries',
            'Topic :: System :: Benchmark',
            'Topic :: System :: Hardware :: Hardware Drivers',
            'Topic :: System :: Hardware',
            'Topic :: System :: Monitoring',
            'Topic :: System :: Networking :: Monitoring :: Hardware Watchdog',
            'Topic :: System :: Networking :: Monitoring',
            'Topic :: System :: Networking',
            'Topic :: System :: Operating System',
            'Topic :: System :: Systems Administration',
            'Topic :: Utilities',
        ],
    )
    if setuptools is not None:
        kwargs.update(
            python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
            extras_require=extras_require,
            zip_safe=False,
        )
    success = False
    try:
        setup(**kwargs)
        success = True
    finally:
        if not success and POSIX and not which('gcc'):
            py3 = "3" if PY3 else ""
            if LINUX:
                if which('dpkg'):
                    missdeps("sudo apt-get install gcc python%s-dev" % py3)
                elif which('rpm'):
                    missdeps("sudo yum install gcc python%s-devel" % py3)
            elif MACOS:
                print(hilite("XCode (https://developer.apple.com/xcode/) "
                             "is not installed"), color="red", file=sys.stderr)
            elif FREEBSD:
                if which('pkg'):
                    missdeps("pkg install gcc python%s" % py3)
                elif which('mport'):   # MidnightBSD
                    missdeps("mport install gcc python%s" % py3)
            elif OPENBSD:
                missdeps("pkg_add -v gcc python%s" % py3)
            elif NETBSD:
                missdeps("pkgin install gcc python%s" % py3)
            elif SUNOS:
                missdeps("sudo ln -s /usr/bin/gcc /usr/local/bin/cc && "
                         "pkg install gcc")


if __name__ == '__main__':
    main()
